Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
122
gaia/apps/sms/views/conversation/js/attachment.js
Executable file
122
gaia/apps/sms/views/conversation/js/attachment.js
Executable file
|
|
@ -0,0 +1,122 @@
|
|||
/** Creates an Attachment object
|
||||
* @param {String} type of attachment (image, video, etc).
|
||||
* @param {String} uri Location or datauri of image to show.
|
||||
* @param {Number} size Size of attachment in bytes.
|
||||
* @return {Attachment} new attachment object.
|
||||
*/
|
||||
|
||||
/*global AttachmentRenderer, MimeMapper, MozActivity, Utils*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
// Path to the folder that stores all saved attachments
|
||||
const ATTACHMENT_FOLDER_PATH = 'sms-attachments/';
|
||||
|
||||
/**
|
||||
* Gets actual base file name (name.extension) from its path.
|
||||
* @param {String} filepath
|
||||
* @return {String} actual base file name i.e name.extension
|
||||
*/
|
||||
function getBaseName(filePath) {
|
||||
if (!filePath) {
|
||||
throw new Error('Filepath is not defined!');
|
||||
}
|
||||
return filePath.substring(filePath.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* set Property of Attachment from given blob and options object.
|
||||
* @param {Object, Object} (blob object, options object)
|
||||
*/
|
||||
function Attachment(blob, options) {
|
||||
options = options || {};
|
||||
this.blob = blob;
|
||||
this.isDraft = !!options.isDraft;
|
||||
this.name = blob.name || options.name || '';
|
||||
|
||||
if (typeof this.name !== 'string') {
|
||||
this.name = '';
|
||||
}
|
||||
|
||||
// force the _renderer property to be non enumerable so that we don't try to
|
||||
// store it in IndexedDB
|
||||
Object.defineProperty(this, '_renderer', { writable: true });
|
||||
}
|
||||
|
||||
Attachment.prototype = {
|
||||
/* private methods */
|
||||
_getAttachmentRenderer: function() {
|
||||
this._renderer = this._renderer || AttachmentRenderer.for(this);
|
||||
return this._renderer;
|
||||
},
|
||||
|
||||
/* public properties */
|
||||
get size() {
|
||||
return this.blob.size;
|
||||
},
|
||||
|
||||
get type() {
|
||||
return Utils.typeFromMimeType(this.blob.type);
|
||||
},
|
||||
|
||||
/* public methods */
|
||||
render: function(readyCallback) {
|
||||
var attachmentRenderer = this._getAttachmentRenderer();
|
||||
|
||||
attachmentRenderer.render().catch(function(e) {
|
||||
console.error('Error occurred while rendering attachment.', e);
|
||||
}).then(readyCallback);
|
||||
|
||||
// We still need this for the case where we render a list of attachments
|
||||
// and right order is important.
|
||||
return attachmentRenderer.getAttachmentContainer();
|
||||
},
|
||||
|
||||
updateFileSize: function() {
|
||||
var attachmentRenderer = this._getAttachmentRenderer();
|
||||
attachmentRenderer.updateFileSize();
|
||||
},
|
||||
|
||||
view: function(options) {
|
||||
// Make sure media is openable and savable even if
|
||||
// file extenion is missing or invalid but mimetype is supported.
|
||||
|
||||
var l10nPromise = this.name ? Promise.resolve(this.name) :
|
||||
document.l10n.formatValue('unnamed-attachment');
|
||||
|
||||
return l10nPromise.then(name => {
|
||||
// Make sure media is openable and savable even if
|
||||
// blob mimetype is unsupported but file extension is valid.
|
||||
var mimetype =
|
||||
MimeMapper.guessTypeFromFileProperties(
|
||||
name,
|
||||
this.blob.type.toLowerCase()
|
||||
);
|
||||
var filename = MimeMapper.ensureFilenameMatchesType(name, mimetype);
|
||||
|
||||
// Override filename, so that every attachment that is saved via "open"
|
||||
// activity will be placed in the single location.
|
||||
filename = ATTACHMENT_FOLDER_PATH + getBaseName(filename);
|
||||
|
||||
var activity = new MozActivity({
|
||||
name: 'open',
|
||||
data: {
|
||||
type: mimetype,
|
||||
filename: filename,
|
||||
blob: this.blob,
|
||||
allowSave: options && options.allowSave
|
||||
}
|
||||
});
|
||||
activity.onerror = function() {
|
||||
console.error('error with open activity', this.error.name);
|
||||
if (this.error.name === 'NO_PROVIDER') {
|
||||
Utils.alert('attachmentOpenError');
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.Attachment = Attachment;
|
||||
}(this));
|
||||
300
gaia/apps/sms/views/conversation/js/attachment_renderer.js
Executable file
300
gaia/apps/sms/views/conversation/js/attachment_renderer.js
Executable file
|
|
@ -0,0 +1,300 @@
|
|||
/*global Promise, Template, Utils,
|
||||
ImageUtils
|
||||
*/
|
||||
|
||||
/* exported AttachmentRenderer */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
// do not create thumbnails for too big attachments
|
||||
// (see bug 805114 for a similar issue in Gallery)
|
||||
const MAX_THUMBNAIL_GENERATION_SIZE = 10 * 1024 * 1024; // 5MB
|
||||
|
||||
// Actual thumbnails size should be 10 rem (100px) * devicePixelRatio
|
||||
const THUMBNAIL_SIZE = 100 * window.devicePixelRatio;
|
||||
|
||||
/**
|
||||
* A <div> container suits most of the cases where we want to display an
|
||||
* MMS attachment (= icon + file name + file size). However, drafts are a
|
||||
* specific case because they are inside an editable area.
|
||||
*
|
||||
* A <div contenteditable="false"> container would be fine for drafts but
|
||||
* Gecko does not support it at the moment, see bug 685445:
|
||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=685445
|
||||
*
|
||||
* Instead of a bunch of JavaScript to manage where the caret is and what
|
||||
* to delete on backspace, the contentEditable region treats the iframe as
|
||||
* a simple block. Outside of the Compose area, a <div> container is still
|
||||
* fine -- and it's *way* faster.
|
||||
*/
|
||||
const RENDERERS = {
|
||||
draft: {
|
||||
createAttachmentContainer: function() {
|
||||
var container = document.createElement('iframe');
|
||||
|
||||
// The attachment's iFrame requires access to the parent document's
|
||||
// context so that URIs for Blobs created in the parent may resolve as
|
||||
// expected.
|
||||
container.setAttribute('sandbox', 'allow-same-origin');
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders baseMarkup into container node (in this case iframe).
|
||||
* @param baseMarkup Base attachment HTML markup. It should be safely
|
||||
* escaped in advance!
|
||||
* @param attachmentContainer Attachment container node.
|
||||
* @returns {Promise.<Node>} Content container node is the container node
|
||||
* for the attachment base HTML markup that allows consumer code to
|
||||
* perform post processing DOM operations.
|
||||
*/
|
||||
renderTo: function(data, attachmentContainer) {
|
||||
attachmentContainer.classList.add(data.cssClass);
|
||||
|
||||
var template = Template('attachment-draft-tmpl');
|
||||
var markup = template.interpolate({
|
||||
baseURL: location.protocol + '//' + location.host + '/',
|
||||
baseHTML: data.markup,
|
||||
cssClass: data.cssClass
|
||||
}, { safe: ['baseHTML'] });
|
||||
|
||||
var blob = new Blob([markup], { type: 'text/html' });
|
||||
var blobUrl = window.URL.createObjectURL(blob);
|
||||
|
||||
attachmentContainer.src = blobUrl;
|
||||
|
||||
return this._whenLoaded(attachmentContainer).then(function onload() {
|
||||
// do some postprocessing after it's loaded
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
|
||||
var contentDocument = attachmentContainer.contentDocument;
|
||||
var documentElement = contentDocument.documentElement;
|
||||
documentElement.lang = document.documentElement.getAttribute('lang');
|
||||
documentElement.dir = document.documentElement.getAttribute('dir');
|
||||
document.l10n.translateFragment(documentElement);
|
||||
|
||||
// Attach click listeners and fire the callback when rendering is
|
||||
// complete: we can't bind `readyCallback' to the `load' event
|
||||
// listener because it would break our unit tests.
|
||||
// Bubble click events from inside the iframe.
|
||||
contentDocument.addEventListener(
|
||||
'click',
|
||||
() => attachmentContainer.click()
|
||||
);
|
||||
|
||||
// Return actual content container node to allow postprocessing of
|
||||
// DOM content without dealing with iframe structure.
|
||||
return contentDocument.body;
|
||||
});
|
||||
},
|
||||
|
||||
_whenLoaded: function(iframe) {
|
||||
var innerDocument = iframe.contentDocument;
|
||||
// checking for the class is a way to check that the correct content is
|
||||
// loaded. If the IFrame is inserted to the DOM before we change the
|
||||
// src, its default src about:blank is loaded and we have a body, albeit
|
||||
// a bad one.
|
||||
if (innerDocument && innerDocument.body &&
|
||||
innerDocument.body.classList.contains('attachment-draft')) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
iframe.addEventListener('load', function onload() {
|
||||
this.removeEventListener('load', onload);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the inner DOM node.
|
||||
*/
|
||||
getContentNode: function(attachmentContainer) {
|
||||
return this._whenLoaded(attachmentContainer).then(
|
||||
() => attachmentContainer.contentDocument.documentElement
|
||||
);
|
||||
},
|
||||
|
||||
setL10nAttributes: function(element, l10nId, l10nArgs) {
|
||||
document.l10n.setAttributes(
|
||||
element, l10nId, l10nArgs
|
||||
);
|
||||
// l10n library can't see changes inside iframes yet
|
||||
document.l10n.translateFragment(element);
|
||||
}
|
||||
},
|
||||
|
||||
base: {
|
||||
createAttachmentContainer: function() {
|
||||
return document.createElement('div');
|
||||
},
|
||||
|
||||
renderTo: function(data, attachmentContainer) {
|
||||
attachmentContainer.classList.add(data.cssClass);
|
||||
attachmentContainer.innerHTML = data.markup;
|
||||
|
||||
return Promise.resolve(attachmentContainer);
|
||||
},
|
||||
|
||||
getContentNode: function(attachmentContainer) {
|
||||
return Promise.resolve(attachmentContainer);
|
||||
},
|
||||
|
||||
setL10nAttributes: function(element, l10nId, l10nArgs) {
|
||||
document.l10n.setAttributes(
|
||||
element, l10nId, l10nArgs
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var AttachmentRenderer = function(attachment) {
|
||||
this._attachment = attachment;
|
||||
this._renderer = attachment.isDraft ? RENDERERS.draft : RENDERERS.base;
|
||||
this._attachmentContainer = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets DOM node that will be container for the attachment markup. This method
|
||||
* will create new container if it wasn't before.
|
||||
* @returns {Node}
|
||||
*/
|
||||
AttachmentRenderer.prototype.getAttachmentContainer = function() {
|
||||
if (!this._attachmentContainer) {
|
||||
this._attachmentContainer = this._createAttachmentContainer();
|
||||
}
|
||||
|
||||
return this._attachmentContainer;
|
||||
};
|
||||
|
||||
AttachmentRenderer.prototype.render = function() {
|
||||
var attachmentContainer = this.getAttachmentContainer();
|
||||
|
||||
// Currently we try to extract thumbnail for image only, for the rest of
|
||||
// types (audio, video and etc.) we display default attachment placeholder.
|
||||
// Video type should be revisited with:
|
||||
// Bug 924609 - Video thumbnails previews are not showing in MMS when
|
||||
// attaching or receiving a video.
|
||||
var thumbnailPromise = this._attachment.type === 'img' &&
|
||||
this._attachment.size < MAX_THUMBNAIL_GENERATION_SIZE ?
|
||||
this.getThumbnail() : Promise.reject();
|
||||
|
||||
return thumbnailPromise.
|
||||
then((thumbnail) => {
|
||||
return {
|
||||
markup: this._getBaseMarkup('attachment-preview-tmpl'),
|
||||
cssClass: 'preview',
|
||||
thumbnail: thumbnail
|
||||
};
|
||||
}).
|
||||
catch((error) => {
|
||||
return {
|
||||
markup: this._getBaseMarkup('attachment-nopreview-tmpl', !!error),
|
||||
cssClass: 'nopreview'
|
||||
};
|
||||
}).
|
||||
then((data) => {
|
||||
return this._renderer.renderTo(data, attachmentContainer).
|
||||
then((contentContainer) => {
|
||||
var thumbnailNode = contentContainer.querySelector('.thumbnail');
|
||||
if (thumbnailNode) {
|
||||
thumbnailNode.style.backgroundImage =
|
||||
'url("' + data.thumbnail.url + data.thumbnail.fragment + '")';
|
||||
|
||||
// It's essential to remember real image URL to revoke it later
|
||||
attachmentContainer.dataset.thumbnail = data.thumbnail.url;
|
||||
}
|
||||
|
||||
var fileNode = contentContainer.querySelector('.file-name');
|
||||
if (this._attachment.name) {
|
||||
fileNode.removeAttribute('data-l10n-id');
|
||||
fileNode.textContent = this._attachment.name.slice(
|
||||
this._attachment.name.lastIndexOf('/') + 1
|
||||
);
|
||||
} else {
|
||||
fileNode.setAttribute('data-l10n-id', 'unnamed-attachment');
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
AttachmentRenderer.prototype.updateFileSize = function() {
|
||||
var attachmentContainer = this.getAttachmentContainer();
|
||||
return this._renderer.getContentNode(attachmentContainer).then(
|
||||
(contentNode) => {
|
||||
var sizeIndicator = contentNode.querySelector('.js-size-indicator');
|
||||
if (!sizeIndicator) {
|
||||
throw new Error('updateFileSize should be called after a render().');
|
||||
}
|
||||
|
||||
var sizeL10n = Utils.getSizeForL10n(this._attachment.size);
|
||||
this._renderer.setL10nAttributes(
|
||||
sizeIndicator, sizeL10n.l10nId, sizeL10n.l10nArgs
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts thumbnail for the image.
|
||||
* TODO: As we started to use mozSampleSize for thumbnail generation we need
|
||||
* to check if we still want to store this thumbnail data (indexedDB)
|
||||
* Bug 876467 - [MMS] generate, store, and reuse image thumbnails
|
||||
* @returns {Promise}
|
||||
*/
|
||||
AttachmentRenderer.prototype.getThumbnail = function() {
|
||||
// The thumbnail format matches the blob format.
|
||||
var blob = this._attachment.blob;
|
||||
|
||||
return ImageUtils.getSizeAndType(blob).then(
|
||||
function getSizeResolve(data) {
|
||||
var fragment = ImageUtils.Downsample.sizeNoMoreThan(
|
||||
THUMBNAIL_SIZE / Math.min(data.width, data.height)
|
||||
);
|
||||
|
||||
return {
|
||||
url: window.URL.createObjectURL(blob),
|
||||
fragment: fragment
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates new attachment container.
|
||||
* @returns {Node}
|
||||
*/
|
||||
AttachmentRenderer.prototype._createAttachmentContainer = function() {
|
||||
var attachmentContainer = this._renderer.createAttachmentContainer();
|
||||
|
||||
attachmentContainer.classList.add('attachment-container');
|
||||
attachmentContainer.dataset.attachmentType = this._attachment.type;
|
||||
|
||||
return attachmentContainer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns HTML string that represents base attachment markup.
|
||||
* @param templateId Id of the template to use.
|
||||
* @param hasError Indicates whether something is wrong with attachment.
|
||||
* @returns {string}
|
||||
*/
|
||||
AttachmentRenderer.prototype._getBaseMarkup = function(templateId, hasError) {
|
||||
// interpolate the #attachment-[no]preview-tmpl template
|
||||
var sizeL10n = Utils.getSizeForL10n(this._attachment.size);
|
||||
return Template(templateId).interpolate({
|
||||
type: this._attachment.type,
|
||||
errorClass: hasError ? 'corrupted' : '',
|
||||
sizeL10nId: sizeL10n.l10nId,
|
||||
sizeL10nArgs: JSON.stringify(sizeL10n.l10nArgs)
|
||||
});
|
||||
};
|
||||
|
||||
exports.AttachmentRenderer = {
|
||||
for: function(attachment) {
|
||||
return new AttachmentRenderer(attachment);
|
||||
}
|
||||
};
|
||||
}(this));
|
||||
848
gaia/apps/sms/views/conversation/js/compose.js
Executable file
848
gaia/apps/sms/views/conversation/js/compose.js
Executable file
|
|
@ -0,0 +1,848 @@
|
|||
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
/*global Settings, Utils, Attachment, MozActivity,
|
||||
MessageManager,
|
||||
SubjectComposer,
|
||||
Promise,
|
||||
EventDispatcher,
|
||||
DOMError,
|
||||
OptionMenu
|
||||
*/
|
||||
/*exported Compose */
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Handle UI specifics of message composition. Namely,
|
||||
* resetting (auto manages placeholder text), getting
|
||||
* message content, and message size
|
||||
*/
|
||||
var Compose = (function() {
|
||||
const TYPES = ['image', 'audio', 'video'];
|
||||
|
||||
// delay between 2 counter updates while composing a message
|
||||
const UPDATE_DELAY = 500;
|
||||
|
||||
// Min available chars count that triggers available chars counter
|
||||
const MIN_AVAILABLE_CHARS_COUNT = 20;
|
||||
|
||||
var placeholderClass = 'placeholder';
|
||||
var attachmentClass = 'attachment-container';
|
||||
|
||||
var attachments = new Map();
|
||||
|
||||
// will be defined in init
|
||||
var dom = {
|
||||
form: null,
|
||||
message: null,
|
||||
sendButton: null,
|
||||
attachButton: null,
|
||||
counter: null
|
||||
};
|
||||
|
||||
var state = {
|
||||
empty: true,
|
||||
maxLength: null,
|
||||
size: null,
|
||||
lastScrollPosition: 0,
|
||||
resizing: false,
|
||||
|
||||
// 'sms' or 'mms'
|
||||
type: 'sms',
|
||||
|
||||
lock: {
|
||||
canSend: null,
|
||||
canEdit: null,
|
||||
forceType: null
|
||||
},
|
||||
|
||||
segmentInfo: {
|
||||
segments: 0,
|
||||
charsAvailableInLastSegment: 0
|
||||
}
|
||||
};
|
||||
|
||||
var subject = null;
|
||||
|
||||
// Given a DOM element, we will extract an array of the
|
||||
// relevant nodes as attachment or text
|
||||
function getContentFromDOM(domElement) {
|
||||
var content = [];
|
||||
var node;
|
||||
|
||||
for (node = domElement.firstChild; node; node = node.nextSibling) {
|
||||
// hunt for an attachment in the Map and append it
|
||||
var attachment = attachments.get(node);
|
||||
if (attachment) {
|
||||
content.push(attachment);
|
||||
continue;
|
||||
}
|
||||
|
||||
var last = content.length - 1;
|
||||
var text = node.textContent;
|
||||
|
||||
// Bug 877141 - contenteditable wil insert non-break spaces when
|
||||
// multiple consecutive spaces are entered, we don't want them.
|
||||
if (text) {
|
||||
text = text.replace(/\u00A0/g, ' ');
|
||||
}
|
||||
|
||||
if (node.nodeName == 'BR') {
|
||||
if (node === domElement.lastChild) {
|
||||
continue;
|
||||
}
|
||||
text = '\n';
|
||||
}
|
||||
|
||||
// append (if possible) text to the last entry
|
||||
if (text.length) {
|
||||
if (typeof content[last] === 'string') {
|
||||
content[last] += text;
|
||||
} else {
|
||||
content.push(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// anytime content changes - takes a parameter to check for image resizing
|
||||
function onContentChanged(duck) {
|
||||
// if the duck is an image attachment or object, handle resizes
|
||||
if ((duck instanceof Attachment && duck.type === 'img') ||
|
||||
(duck && duck.containsImage)) {
|
||||
return imageAttachmentsHandling();
|
||||
}
|
||||
|
||||
var isEmptyMessage = !dom.message.textContent.length && !hasAttachment();
|
||||
|
||||
if (isEmptyMessage) {
|
||||
var brs = dom.message.getElementsByTagName('br');
|
||||
// firefox will keep an extra <br> in there
|
||||
if (brs.length > 1) {
|
||||
isEmptyMessage = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder management
|
||||
var placeholding = dom.message.classList.contains(placeholderClass);
|
||||
if (placeholding && !isEmptyMessage) {
|
||||
dom.message.classList.remove(placeholderClass);
|
||||
}
|
||||
if (!placeholding && isEmptyMessage) {
|
||||
dom.message.classList.add(placeholderClass);
|
||||
}
|
||||
|
||||
state.emptyMessage = isEmptyMessage;
|
||||
|
||||
Compose.updateEmptyState();
|
||||
Compose.updateType();
|
||||
Compose.updateSendButton();
|
||||
Compose.updateAttachButton();
|
||||
updateSegmentInfoThrottled();
|
||||
|
||||
Compose.emit('input');
|
||||
}
|
||||
|
||||
function onSubjectChanged() {
|
||||
Compose.updateEmptyState();
|
||||
Compose.updateType();
|
||||
Compose.updateSendButton();
|
||||
Compose.updateAttachButton();
|
||||
|
||||
Compose.emit('subject-change');
|
||||
}
|
||||
|
||||
function onSubjectVisibilityChanged() {
|
||||
if (subject.isVisible()) {
|
||||
dom.form.classList.add('subject-composer-visible');
|
||||
} else {
|
||||
dom.message.focus();
|
||||
dom.form.classList.remove('subject-composer-visible');
|
||||
}
|
||||
}
|
||||
|
||||
function hasAttachment() {
|
||||
return !!dom.message.querySelector('iframe');
|
||||
}
|
||||
|
||||
function hasSubject() {
|
||||
return subject.isVisible() && !!subject.getValue();
|
||||
}
|
||||
|
||||
function composeKeyEvents(e) {
|
||||
// If message can't be edited anymore and no-backspace pressed, cancel.
|
||||
if (!compose.canEdit() && e.which !== 8) {
|
||||
e.preventDefault();
|
||||
} else {
|
||||
// Trigger a recompute of size on the keypress events.
|
||||
state.size = null;
|
||||
}
|
||||
}
|
||||
|
||||
function insert(item) {
|
||||
var fragment = document.createDocumentFragment();
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// trigger recalc on insert
|
||||
state.size = null;
|
||||
|
||||
if (item.render) { // it's an Attachment
|
||||
var node = item.render();
|
||||
attachments.set(node, item);
|
||||
fragment.appendChild(node);
|
||||
} else if (item.nodeName === 'IFRAME') {
|
||||
// this iframe is generated by us
|
||||
fragment.appendChild(item);
|
||||
} else if (typeof item === 'string') {
|
||||
item.split('\n').forEach(function(line) {
|
||||
fragment.appendChild(document.createTextNode(line));
|
||||
fragment.appendChild(document.createElement('br'));
|
||||
});
|
||||
fragment.lastElementChild.remove();
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
function imageAttachmentsHandling() {
|
||||
// There is need to resize image attachment if total compose
|
||||
// size doen't exceed mms size limitation.
|
||||
if (Compose.size < Settings.mmsSizeLimitation) {
|
||||
onContentChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
var imgNodes = [];
|
||||
attachments.forEach((attachment, node) => {
|
||||
if (attachment.type === 'img') {
|
||||
imgNodes.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
var done = 0;
|
||||
|
||||
// Total number of images < 3
|
||||
// => Set max image size to 2/5 message size limitation.
|
||||
// Total number of images >= 3
|
||||
// => Set max image size to 1/5 message size limitation.
|
||||
var images = imgNodes.length;
|
||||
var limit = images > 2 ? Settings.mmsSizeLimitation * 0.2 :
|
||||
Settings.mmsSizeLimitation * 0.4;
|
||||
|
||||
function imageSized() {
|
||||
if (++done === images) {
|
||||
state.resizing = false;
|
||||
onContentChanged();
|
||||
} else {
|
||||
resizedImg(imgNodes[done]);
|
||||
}
|
||||
}
|
||||
|
||||
function resizedImg(node) {
|
||||
var item = attachments.get(node);
|
||||
if (item.blob.size < limit) {
|
||||
imageSized();
|
||||
} else {
|
||||
Utils.getResizedImgBlob(item.blob, limit, function(resizedBlob) {
|
||||
// trigger recalc when resized
|
||||
state.size = null;
|
||||
|
||||
item.blob = resizedBlob;
|
||||
item.updateFileSize();
|
||||
|
||||
imageSized();
|
||||
});
|
||||
}
|
||||
}
|
||||
state.resizing = true;
|
||||
resizedImg(imgNodes[0]);
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
var segmentInfoTimeout = null;
|
||||
function updateSegmentInfoThrottled() {
|
||||
// we need to call updateSegmentInfo even in MMS mode if we have only text:
|
||||
// if we're in MMS mode because we have a long message, then we need to
|
||||
// check when we go back to SMS mode by having a shorter message.
|
||||
// A possible solution is to do it only when the user deletes characters in
|
||||
// MMS mode.
|
||||
if (hasAttachment()) {
|
||||
return resetSegmentInfo();
|
||||
}
|
||||
|
||||
if (segmentInfoTimeout === null) {
|
||||
segmentInfoTimeout = setTimeout(updateSegmentInfo, UPDATE_DELAY);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSegmentInfo() {
|
||||
segmentInfoTimeout = null;
|
||||
|
||||
var value = Compose.getText();
|
||||
|
||||
// saving one IPC call when we clear the composer
|
||||
var segmentInfoPromise = value ?
|
||||
MessageManager.getSegmentInfo(value) :
|
||||
Promise.reject();
|
||||
|
||||
segmentInfoPromise.then(
|
||||
function(segmentInfo) {
|
||||
state.segmentInfo = segmentInfo;
|
||||
}, resetSegmentInfo
|
||||
).then(compose.updateType.bind(Compose))
|
||||
.then(compose.emit.bind(compose, 'segmentinfochange'));
|
||||
}
|
||||
|
||||
function resetSegmentInfo() {
|
||||
state.segmentInfo = {
|
||||
segments: 0,
|
||||
charsAvailableInLastSegment: 0
|
||||
};
|
||||
}
|
||||
|
||||
function disposeAttachmentNode(attachmentNode) {
|
||||
var thumbnailURL = attachmentNode.dataset.thumbnail;
|
||||
if (thumbnailURL) {
|
||||
window.URL.revokeObjectURL(thumbnailURL);
|
||||
}
|
||||
attachments.delete(attachmentNode);
|
||||
}
|
||||
|
||||
var compose = {
|
||||
init: function composeInit(formId) {
|
||||
dom.form = document.getElementById(formId);
|
||||
dom.message = document.getElementById('messages-input');
|
||||
dom.sendButton = document.getElementById('messages-send-button');
|
||||
dom.attachButton = document.getElementById('messages-attach-button');
|
||||
dom.counter = dom.form.querySelector('.js-message-counter');
|
||||
dom.messagesAttach = dom.form.querySelector('.messages-attach-container');
|
||||
dom.composerButton = dom.form.querySelector('.composer-button-container');
|
||||
|
||||
subject = new SubjectComposer(
|
||||
dom.form.querySelector('.js-subject-composer')
|
||||
);
|
||||
|
||||
subject.on('change', onSubjectChanged);
|
||||
subject.on('visibility-change', onSubjectChanged);
|
||||
subject.on('visibility-change', onSubjectVisibilityChanged);
|
||||
|
||||
// update the placeholder, send button and Compose.type
|
||||
dom.message.addEventListener('input', onContentChanged);
|
||||
|
||||
// we need to bind to keydown & keypress because of #870120
|
||||
dom.message.addEventListener('keydown', composeKeyEvents);
|
||||
dom.message.addEventListener('keypress', composeKeyEvents);
|
||||
|
||||
dom.message.addEventListener('click',
|
||||
this.onAttachmentClick.bind(this));
|
||||
|
||||
dom.attachButton.addEventListener('click',
|
||||
this.onAttachClick.bind(this));
|
||||
|
||||
this.offAll();
|
||||
this.clear();
|
||||
|
||||
this.on('type', this.onTypeChange.bind(this));
|
||||
this.on('type', this.updateMessageCounter.bind(this));
|
||||
this.on('segmentinfochange', this.updateMessageCounter.bind(this));
|
||||
|
||||
var onInteracted = this.emit.bind(this, 'interact');
|
||||
|
||||
dom.message.addEventListener('focus', onInteracted);
|
||||
dom.message.addEventListener('click', onInteracted);
|
||||
dom.messagesAttach.addEventListener('click', onInteracted);
|
||||
dom.composerButton.addEventListener('click', onInteracted);
|
||||
subject.on('focus', onInteracted);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
getContent: function() {
|
||||
return getContentFromDOM(dom.message);
|
||||
},
|
||||
|
||||
getSubject: function() {
|
||||
return subject.isVisible() ? subject.getValue() : null;
|
||||
},
|
||||
|
||||
setSubject: function(value) {
|
||||
return subject.setValue(value);
|
||||
},
|
||||
|
||||
isSubjectMaxLength: function() {
|
||||
return subject.getValue().length >= subject.getMaxLength();
|
||||
},
|
||||
|
||||
showSubject: function() {
|
||||
subject.show();
|
||||
subject.focus();
|
||||
},
|
||||
|
||||
hideSubject: function() {
|
||||
subject.hide();
|
||||
},
|
||||
|
||||
/** Render draft
|
||||
*
|
||||
* @param {Draft} draft Draft to be loaded into the composer.
|
||||
*
|
||||
*/
|
||||
fromDraft: function(draft) {
|
||||
// Clear out the composer
|
||||
this.clear();
|
||||
|
||||
// If we don't have a draft, return only having cleared the composer
|
||||
if (!draft) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (draft.subject) {
|
||||
this.setSubject(draft.subject);
|
||||
subject.show();
|
||||
}
|
||||
|
||||
if (!draft.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(draft.content)) {
|
||||
throw new Error('If present, the `content` property must be an array.');
|
||||
}
|
||||
|
||||
// draft content is an array
|
||||
draft.content.forEach(function(fragment) {
|
||||
// If the fragment is an attachment
|
||||
// use the stored content to instantiate a new Attachment object
|
||||
// to be properly rendered after a cold start for the app
|
||||
if (fragment.blob) {
|
||||
fragment = new Attachment(fragment.blob, {
|
||||
name: fragment.name,
|
||||
isDraft: true
|
||||
});
|
||||
}
|
||||
// Append each fragment in order to the composer
|
||||
Compose.append(fragment);
|
||||
}, Compose);
|
||||
},
|
||||
|
||||
getText: function() {
|
||||
var out = this.getContent().filter(function(elem) {
|
||||
return (typeof elem === 'string');
|
||||
});
|
||||
return out.join('');
|
||||
},
|
||||
|
||||
isEmpty: function() {
|
||||
return state.empty;
|
||||
},
|
||||
|
||||
setupLock({ canSend, canEdit, forceType }) {
|
||||
state.lock.canSend = canSend;
|
||||
state.lock.canEdit = canEdit;
|
||||
state.lock.forceType = forceType;
|
||||
},
|
||||
|
||||
refresh() {
|
||||
this.updateType();
|
||||
this.updateSendButton();
|
||||
this.updateAttachButton();
|
||||
},
|
||||
|
||||
scrollToTarget: function(target) {
|
||||
// target can be an element or a selection range
|
||||
var targetRect = target.getBoundingClientRect();
|
||||
|
||||
// put the middle of the target at the middle of the container box
|
||||
var containerRect = dom.message.getBoundingClientRect();
|
||||
var offset = (targetRect.top + targetRect.height / 2) -
|
||||
(containerRect.top + containerRect.height / 2);
|
||||
|
||||
// we += because the scrollTop that was set is already compensated
|
||||
// with the getBoundingClientRect()
|
||||
dom.message.scrollTop += offset;
|
||||
},
|
||||
|
||||
scrollMessageContent: function() {
|
||||
if (document.activeElement === dom.message) {
|
||||
// we just got the focus: ensure the caret is visible, and clone
|
||||
// the range to avoid any unnecessary selectionchange event fired
|
||||
// by modifying the range associated with current selection
|
||||
var range = window.getSelection().getRangeAt(0).cloneRange();
|
||||
if (range.collapsed) {
|
||||
// We can't get the bounding client rect of a collapsed range,
|
||||
// so let's insert a temporary node to get the caret position.
|
||||
range.insertNode(document.createElement('span'));
|
||||
this.scrollToTarget(range);
|
||||
range.deleteContents();
|
||||
} else {
|
||||
this.scrollToTarget(range);
|
||||
}
|
||||
state.lastScrollPosition = dom.message.scrollTop;
|
||||
} else {
|
||||
// we just lost the focus: restore the last scroll position
|
||||
dom.message.scrollTop = state.lastScrollPosition;
|
||||
}
|
||||
},
|
||||
|
||||
/** Writes node to composition element
|
||||
* @param {mixed} item Html, DOMNode, or attachment to add
|
||||
* to composition element.
|
||||
* @param {Boolean} position True to append, false to prepend or
|
||||
* undefined/null for auto (at cursor).
|
||||
*/
|
||||
prepend: function(item) {
|
||||
var fragment = insert(item);
|
||||
if (!fragment) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// If the first element is a <br>, it needs to stay first
|
||||
// insert after it but before everyting else
|
||||
if (dom.message.firstChild && dom.message.firstChild.nodeName === 'BR') {
|
||||
dom.message.insertBefore(fragment, dom.message.childNodes[1]);
|
||||
} else {
|
||||
dom.message.insertBefore(fragment, dom.message.childNodes[0]);
|
||||
}
|
||||
|
||||
onContentChanged(item);
|
||||
return this;
|
||||
},
|
||||
|
||||
// if item is an array,ignore calling onContentChanged for each item
|
||||
append: function(item, options) {
|
||||
options = options || {};
|
||||
if (Array.isArray(item)) {
|
||||
var containsImage = false;
|
||||
item.forEach(function(content) {
|
||||
if (content.type === 'img') {
|
||||
containsImage = true;
|
||||
}
|
||||
this.append(content, {ignoreChange: true});
|
||||
}, this);
|
||||
onContentChanged({containsImage: containsImage});
|
||||
} else {
|
||||
var fragment = insert(item);
|
||||
if (!fragment) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (document.activeElement === dom.message) {
|
||||
// insert element at caret position
|
||||
var range = window.getSelection().getRangeAt(0);
|
||||
var firstNodes = fragment.firstChild;
|
||||
range.deleteContents();
|
||||
range.insertNode(fragment);
|
||||
this.scrollToTarget(range);
|
||||
dom.message.focus();
|
||||
range.setStartAfter(firstNodes);
|
||||
} else {
|
||||
// insert element at the end of the Compose area
|
||||
dom.message.insertBefore(fragment, dom.message.lastChild);
|
||||
this.scrollToTarget(dom.message.lastChild);
|
||||
}
|
||||
if (!options.ignoreChange) {
|
||||
onContentChanged(item);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
clear: function() {
|
||||
// changing the type here prevents the "type" event from being fired
|
||||
state.type = 'sms';
|
||||
this.onTypeChange();
|
||||
|
||||
// Dispose attachments
|
||||
attachments.forEach((attachment, node) => disposeAttachmentNode(node));
|
||||
|
||||
dom.message.innerHTML = '<br>';
|
||||
|
||||
subject.reset();
|
||||
|
||||
state.resizing = false;
|
||||
state.size = 0;
|
||||
|
||||
resetSegmentInfo();
|
||||
segmentInfoTimeout = null;
|
||||
|
||||
onContentChanged();
|
||||
return this;
|
||||
},
|
||||
|
||||
focus: function() {
|
||||
dom.message.focus();
|
||||
|
||||
// Put the cursor at the end of the message
|
||||
var selection = window.getSelection();
|
||||
var range = document.createRange();
|
||||
var lastChild = dom.message.lastChild;
|
||||
if (lastChild.tagName === 'BR') {
|
||||
range.setStartBefore(lastChild);
|
||||
} else {
|
||||
range.setStartAfter(lastChild);
|
||||
}
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
updateType: function() {
|
||||
var newType = this.getType();
|
||||
if (newType !== state.type) {
|
||||
state.type = newType;
|
||||
this.emit('type');
|
||||
}
|
||||
},
|
||||
|
||||
getType() {
|
||||
var typeForcedByLock = state.lock.forceType && state.lock.forceType();
|
||||
if (typeForcedByLock) {
|
||||
return typeForcedByLock;
|
||||
}
|
||||
|
||||
var isTextTooLong =
|
||||
state.segmentInfo.segments > Settings.maxConcatenatedMessages;
|
||||
|
||||
return isTextTooLong || hasSubject() || hasAttachment() ? 'mms' : 'sms';
|
||||
},
|
||||
|
||||
updateEmptyState: function() {
|
||||
state.empty = state.emptyMessage && !hasSubject();
|
||||
},
|
||||
|
||||
updateSendButton: function() {
|
||||
dom.sendButton.disabled = !this.canSend();
|
||||
},
|
||||
|
||||
updateAttachButton: function() {
|
||||
dom.attachButton.disabled = !this.canEdit();
|
||||
},
|
||||
|
||||
canSend() {
|
||||
var isSendAllowedByLock = !state.lock.canSend || state.lock.canSend();
|
||||
|
||||
var sizeExceedsLimit = this.type === 'mms' &&
|
||||
Settings.mmsSizeLimitation && this.size > Settings.mmsSizeLimitation;
|
||||
|
||||
// We can't send if message is empty, we're in the process of resizing or
|
||||
// message is already too big.
|
||||
return isSendAllowedByLock && !sizeExceedsLimit && !this.isEmpty() &&
|
||||
!this.isResizing;
|
||||
},
|
||||
|
||||
canEdit() {
|
||||
var isEditAllowedByLock = !state.lock.canEdit || state.lock.canEdit();
|
||||
|
||||
var sizeExceedsOrEqualToLimit = this.type === 'mms' &&
|
||||
Settings.mmsSizeLimitation && this.size >= Settings.mmsSizeLimitation;
|
||||
|
||||
// User can continue to type message if it's either SMS, MMS which size
|
||||
// doesn't exceed maximum allowed size or maximum allowed size is not set.
|
||||
return isEditAllowedByLock && !sizeExceedsOrEqualToLimit;
|
||||
},
|
||||
|
||||
_onAttachmentRequestError: function c_onAttachmentRequestError(err) {
|
||||
var errId = err instanceof DOMError ? err.name : err.message;
|
||||
if (errId === 'file too large') {
|
||||
Utils.alert({
|
||||
id: 'attached-files-too-large',
|
||||
args: {
|
||||
n: 1,
|
||||
mmsSize: (Settings.mmsSizeLimitation / 1024).toFixed(0)
|
||||
}
|
||||
});
|
||||
|
||||
//'pick cancelled' is the error thrown when the pick activity app is
|
||||
// canceled normally
|
||||
} else if (errId !== 'ActivityCanceled' && errId !== 'pick cancelled') {
|
||||
console.warn('Unhandled error: ', err);
|
||||
}
|
||||
},
|
||||
|
||||
onAttachClick: function c_onAttachClick(event) {
|
||||
this.requestAttachment().then(
|
||||
this.append.bind(this),
|
||||
this._onAttachmentRequestError
|
||||
);
|
||||
},
|
||||
|
||||
onAttachmentClick: function c_onAttachmentClick(event) {
|
||||
if (!event.target.classList.contains(attachmentClass) || state.resizing) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentAttachmentDOM = event.target;
|
||||
var currentAttachment = attachments.get(event.target);
|
||||
var name = currentAttachment.name;
|
||||
var blob = currentAttachment.blob;
|
||||
var options = {
|
||||
header: name.substr(name.lastIndexOf('/') + 1)
|
||||
};
|
||||
|
||||
// Localize the name of the file type
|
||||
var mimeFirstPart = blob.type.substr(0, blob.type.indexOf('/'));
|
||||
var fileType = (TYPES.indexOf(mimeFirstPart) > -1) ?
|
||||
mimeFirstPart: 'other';
|
||||
|
||||
var onView = () => { currentAttachment.view(); };
|
||||
|
||||
function onRemove() {
|
||||
dom.message.removeChild(currentAttachmentDOM);
|
||||
disposeAttachmentNode(currentAttachmentDOM);
|
||||
state.size = null;
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
var onReplace = () => {
|
||||
this.requestAttachment().then((newAttachment) => {
|
||||
var fragment = insert(newAttachment);
|
||||
|
||||
dom.message.insertBefore(fragment, currentAttachmentDOM);
|
||||
dom.message.removeChild(currentAttachmentDOM);
|
||||
disposeAttachmentNode(currentAttachmentDOM);
|
||||
onContentChanged(newAttachment);
|
||||
}, this._onAttachmentRequestError);
|
||||
};
|
||||
|
||||
options.items = [
|
||||
{ l10nId: `view-attachment-${fileType}`, method: onView },
|
||||
{ l10nId: `remove-attachment-${fileType}`, method: onRemove },
|
||||
{ l10nId: `replace-attachment-${fileType}`, method: onReplace },
|
||||
{ l10nId: 'cancel' }
|
||||
];
|
||||
|
||||
new OptionMenu(options).show();
|
||||
},
|
||||
|
||||
onTypeChange: function c_onTypeChange() {
|
||||
if (this.type === 'sms') {
|
||||
dom.message.setAttribute('x-inputmode', '-moz-sms');
|
||||
} else {
|
||||
dom.message.removeAttribute('x-inputmode');
|
||||
}
|
||||
|
||||
dom.form.dataset.messageType = this.type;
|
||||
},
|
||||
|
||||
updateMessageCounter: function c_updateMessageCounter() {
|
||||
var counterValue = '';
|
||||
|
||||
if (this.type === 'sms') {
|
||||
var segments = state.segmentInfo.segments;
|
||||
var availableChars = state.segmentInfo.charsAvailableInLastSegment;
|
||||
|
||||
if (segments && (segments > 1 ||
|
||||
availableChars <= MIN_AVAILABLE_CHARS_COUNT)) {
|
||||
counterValue = availableChars + '/' + segments;
|
||||
}
|
||||
}
|
||||
|
||||
if (counterValue !== dom.counter.textContent) {
|
||||
dom.counter.textContent = counterValue;
|
||||
}
|
||||
},
|
||||
|
||||
/** Initiates a 'pick' MozActivity allowing the user to create an
|
||||
* attachment
|
||||
* @return {Promise} A Promise for the underlying DOMRequest API.
|
||||
* Resolve will be triggered when activity returns
|
||||
* successfully with result blob size smaller than limit.
|
||||
* Reject will be triggered when activity returns error
|
||||
* or blob size exceeds limit.
|
||||
*/
|
||||
requestAttachment: function() {
|
||||
var activityData = {
|
||||
type: ['image/*', 'audio/*', 'video/*', 'text/vcard']
|
||||
};
|
||||
var activity;
|
||||
var defer = Utils.Promise.defer();
|
||||
|
||||
if (Settings.mmsSizeLimitation) {
|
||||
activityData.maxFileSizeBytes = Settings.mmsSizeLimitation;
|
||||
}
|
||||
|
||||
activity = new MozActivity({
|
||||
name: 'pick',
|
||||
data: activityData
|
||||
});
|
||||
|
||||
activity.onsuccess = function() {
|
||||
var result = activity.result;
|
||||
|
||||
if (Settings.mmsSizeLimitation &&
|
||||
result.blob.size > Settings.mmsSizeLimitation &&
|
||||
Utils.typeFromMimeType(result.blob.type) !== 'img') {
|
||||
|
||||
defer.reject(new Error('file too large'));
|
||||
}
|
||||
|
||||
defer.resolve(new Attachment(result.blob, {
|
||||
name: result.name,
|
||||
isDraft: true
|
||||
}));
|
||||
};
|
||||
|
||||
// Re-throw Gecko-level errors
|
||||
activity.onerror = () => {
|
||||
defer.reject(activity.error);
|
||||
};
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Object.defineProperty(compose, 'type', {
|
||||
get: function composeGetType() {
|
||||
return state.type;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(compose, 'size', {
|
||||
get: function composeGetSize() {
|
||||
if (state.size === null) {
|
||||
state.size = this.getContent().reduce(function(sum, content) {
|
||||
if (typeof content === 'string') {
|
||||
return sum + content.length;
|
||||
} else {
|
||||
return sum + content.size;
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return state.size;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(compose, 'segmentInfo', {
|
||||
get: function composeGetSegmentInfo() {
|
||||
return state.segmentInfo;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(compose, 'isResizing', {
|
||||
get: function composeGetResizeState() {
|
||||
return state.resizing;
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(compose, 'isSubjectVisible', {
|
||||
get: function composeIsSubjectVisible() {
|
||||
return subject.isVisible();
|
||||
}
|
||||
});
|
||||
|
||||
return EventDispatcher.mixin(compose, [
|
||||
'input',
|
||||
'type',
|
||||
'segmentinfochange',
|
||||
'interact',
|
||||
'subject-change'
|
||||
]);
|
||||
}());
|
||||
3167
gaia/apps/sms/views/conversation/js/conversation.js
Executable file
3167
gaia/apps/sms/views/conversation/js/conversation.js
Executable file
File diff suppressed because it is too large
Load diff
582
gaia/apps/sms/views/conversation/js/information.js
Executable file
582
gaia/apps/sms/views/conversation/js/information.js
Executable file
|
|
@ -0,0 +1,582 @@
|
|||
/*global ContactRenderer,
|
||||
Contacts,
|
||||
ConversationView,
|
||||
MessageManager,
|
||||
MobileOperator,
|
||||
Navigation,
|
||||
Settings,
|
||||
Template,
|
||||
Threads,
|
||||
Utils
|
||||
*/
|
||||
|
||||
/*exported Information */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
const MONTH = 30 * DAY;
|
||||
const LATENESS_DISPLAY_LOWER_LIMIT = 5 * MINUTE;
|
||||
|
||||
/*
|
||||
Information view module is a subpanel belongs to TheadUI panel. This
|
||||
module provide some default method for thiw view module:
|
||||
|
||||
show: Reuse the conversation view for information view. Hiding unrelated icon,
|
||||
reset the header, and render container for showing the view.
|
||||
|
||||
reset: Hide the view container, clean up the contact list and resume the
|
||||
icons/header for ConversationView.
|
||||
|
||||
renderContactList(participants, options): Rendering group-view style contact
|
||||
list inside the page. Participants is an array of contact number and option
|
||||
is for setting additional information for rendering other lock for contact
|
||||
information.
|
||||
*/
|
||||
var TMPL = function createTemplate(tmpls) {
|
||||
for (var key in tmpls) {
|
||||
tmpls[key] = Template(tmpls[key]);
|
||||
}
|
||||
return tmpls;
|
||||
}({
|
||||
number: 'messages-number-tmpl',
|
||||
report: 'information-report-tmpl'
|
||||
});
|
||||
|
||||
/*
|
||||
Summarized single status based on delivery and read status for mms.
|
||||
The 1st level properties represents delivery status and 2nd level properties
|
||||
represents read status.
|
||||
*/
|
||||
const REPORT_MAP = {
|
||||
'not-applicable': {
|
||||
'not-applicable': 'not-applicable',
|
||||
'pending' : 'pending',
|
||||
'success' : 'read',
|
||||
'error' : 'error'
|
||||
},
|
||||
'pending' : {
|
||||
'not-applicable': 'pending',
|
||||
'pending': 'pending',
|
||||
'success' : 'read', // should not possible
|
||||
'error' : 'error'
|
||||
},
|
||||
'success' : {
|
||||
'not-applicable': 'delivered',
|
||||
'pending': 'delivered',
|
||||
'success' : 'read',
|
||||
'error' : 'error'
|
||||
},
|
||||
'error' : {
|
||||
'not-applicable': 'error',
|
||||
'pending': 'error',
|
||||
'success' : 'error', // should not possible
|
||||
'error' : 'error' // should not possible
|
||||
}
|
||||
};
|
||||
|
||||
// Register the message events we wanted for report view refresh
|
||||
const MESSAGE_EVENTS = [
|
||||
'message-failed-to-send',
|
||||
'message-delivered',
|
||||
'message-read',
|
||||
'message-sent',
|
||||
'message-sending'
|
||||
];
|
||||
|
||||
const reportDateTimeFormatOptions = {
|
||||
month: 'long',
|
||||
day: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
};
|
||||
|
||||
function completeLocaleFormat(timestamp) {
|
||||
var options = Object.assign(
|
||||
{ hour12: navigator.mozHour12 },
|
||||
reportDateTimeFormatOptions);
|
||||
|
||||
return new Date(+timestamp).toLocaleString(navigator.languages, options);
|
||||
}
|
||||
|
||||
function l10nContainsDateSetup(element, timestamp) {
|
||||
element.dataset.l10nDate = timestamp;
|
||||
element.dataset.l10nDateFormat = JSON.stringify(reportDateTimeFormatOptions);
|
||||
element.textContent = completeLocaleFormat(timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate report Div contains delivery report and read report for showing
|
||||
* report information within contact list
|
||||
*/
|
||||
function createReportDiv(reports) {
|
||||
var reportDiv = document.createElement('div');
|
||||
reportDiv.className = 'network-status';
|
||||
var data = {
|
||||
titleL10n: '',
|
||||
reportDateL10n: '',
|
||||
messageL10nDateFormat: JSON.stringify(reportDateTimeFormatOptions),
|
||||
timestamp: ''
|
||||
};
|
||||
var status;
|
||||
var deliveryStatus = reports.deliveryStatus;
|
||||
var readStatus = reports.readStatus;
|
||||
|
||||
if (!readStatus) { // sms
|
||||
status = deliveryStatus === 'success' ?
|
||||
'delivered' :
|
||||
deliveryStatus;
|
||||
} else if (deliveryStatus === 'rejected') {
|
||||
// Status = 'rejected' when receiver is not allowed to download any mms
|
||||
status = 'rejected';
|
||||
} else if (deliveryStatus in REPORT_MAP) {
|
||||
status = REPORT_MAP[deliveryStatus][readStatus];
|
||||
} else {
|
||||
console.error('Invalid message report status: ' + deliveryStatus);
|
||||
return reportDiv;
|
||||
}
|
||||
reportDiv.dataset.deliveryStatus = status;
|
||||
|
||||
switch (status) {
|
||||
case 'not-applicable':
|
||||
return reportDiv;
|
||||
case 'delivered':
|
||||
data.timestamp = '' + reports.deliveryTimestamp;
|
||||
data.reportDateL10n = completeLocaleFormat(reports.deliveryTimestamp);
|
||||
break;
|
||||
case 'read':
|
||||
data.timestamp = '' + reports.readTimestamp;
|
||||
data.reportDateL10n = completeLocaleFormat(reports.readTimestamp);
|
||||
break;
|
||||
}
|
||||
data.titleL10n = 'report-status-' + status;
|
||||
reportDiv.innerHTML = TMPL.report.interpolate(data);
|
||||
|
||||
return reportDiv;
|
||||
}
|
||||
|
||||
function showSimInfo(element, iccId) {
|
||||
var iccManager = navigator.mozIccManager;
|
||||
// Hide the element when single SIM or no iccManager/mobileConnections
|
||||
if (!(Settings.hasSeveralSim() && iccId && iccManager)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var simInfoElement = element.querySelector('.sim-detail');
|
||||
var simId = Settings.getServiceIdByIccId(iccId);
|
||||
|
||||
if (simId === null) {
|
||||
document.l10n.setAttributes(
|
||||
simInfoElement,
|
||||
'dsds-unknown-sim'
|
||||
);
|
||||
} else {
|
||||
var operator = getOperatorByServiceId(simId);
|
||||
var icc = iccManager.getIccById(iccId);
|
||||
var number = icc && icc.iccInfo.msisdn;
|
||||
|
||||
var data = {};
|
||||
var l10nId;
|
||||
|
||||
var info = [operator, number].filter(function(value) {
|
||||
return value;
|
||||
});
|
||||
|
||||
var detailString = info.join(', ');
|
||||
l10nId = info.length ? 'sim-detail' : 'sim-id-label';
|
||||
data = { id: simId + 1, detailString: detailString };
|
||||
document.l10n.setAttributes(
|
||||
simInfoElement,
|
||||
l10nId,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
element.classList.remove('hide');
|
||||
}
|
||||
|
||||
/**
|
||||
* Incoming message: return array of sender number string;
|
||||
* Outgoing message: return array of object(number and report div block).
|
||||
*/
|
||||
function createListWithMsgInfo(message) {
|
||||
var list = [];
|
||||
if (message.delivery === 'received' ||
|
||||
message.delivery === 'not-downloaded') { // received message
|
||||
if (message.type === 'mms') {
|
||||
if (message.receivers) {
|
||||
list.push(...message.receivers);
|
||||
}
|
||||
if (!list.includes(message.sender)) {
|
||||
list.push(message.sender);
|
||||
}
|
||||
} else {
|
||||
list.push(message.sender);
|
||||
}
|
||||
} else if (message.type === 'mms') { // sent mms message
|
||||
message.deliveryInfo.forEach(function(info) {
|
||||
list.push({number: info.receiver,
|
||||
infoBlock: createReportDiv(info)});
|
||||
});
|
||||
} else { // sent sms message
|
||||
var info = {};
|
||||
info.deliveryStatus = message.deliveryStatus;
|
||||
info.deliveryTimestamp = message.deliveryTimestamp;
|
||||
list.push({number: message.receiver,
|
||||
infoBlock: createReportDiv(info)});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will return operator name depending on the iccId.
|
||||
* Will return the empty string in a single SIM scenario.
|
||||
*/
|
||||
function getOperatorByServiceId(index) {
|
||||
var conn = navigator.mozMobileConnections[index];
|
||||
return MobileOperator.userFacingInfo(conn).operator;
|
||||
}
|
||||
|
||||
var VIEWS = {
|
||||
group: {
|
||||
name: 'group-view',
|
||||
selector: '.panel-GroupView',
|
||||
contactFlavor: 'group-view',
|
||||
|
||||
render: function renderGroup() {
|
||||
var participants = Threads.get(this.id).participants;
|
||||
this.renderContactList(participants);
|
||||
document.l10n.setAttributes(
|
||||
this.headerText, 'participant', { n:participants.length }
|
||||
);
|
||||
},
|
||||
|
||||
setEventListener: function setEventListener() {
|
||||
this.contactList.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
var target = event.target;
|
||||
|
||||
ConversationView.promptContact({
|
||||
number: target.dataset.number
|
||||
}).then(
|
||||
() => Navigation.toPanel('thread', { id: this.id })
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
elements: ['contact-list', 'header', 'header-text']
|
||||
},
|
||||
report: {
|
||||
name: 'report-view',
|
||||
selector: '.panel-ReportView',
|
||||
contactFlavor: 'report-view',
|
||||
|
||||
init: function() {
|
||||
this.onStatusChanged = this.onStatusChanged.bind(this);
|
||||
},
|
||||
|
||||
beforeEnter: function() {
|
||||
this.constructor.prototype.beforeEnter.apply(this, arguments);
|
||||
|
||||
MESSAGE_EVENTS.forEach((event) => {
|
||||
MessageManager.on(event, this.onStatusChanged);
|
||||
});
|
||||
},
|
||||
|
||||
afterLeave: function() {
|
||||
this.constructor.prototype.afterLeave.apply(this, arguments);
|
||||
|
||||
MESSAGE_EVENTS.forEach((event) => {
|
||||
MessageManager.off(event, this.onStatusChanged);
|
||||
});
|
||||
},
|
||||
|
||||
render: function renderReport() {
|
||||
var setL10nAttributes = document.l10n.setAttributes;
|
||||
var request = MessageManager.getMessage(this.id);
|
||||
|
||||
// Hide these dynamic fields to avoid incorrect info displayed at first
|
||||
this.subject.classList.add('hide');
|
||||
this.sizeBlock.classList.add('hide');
|
||||
|
||||
request.onsuccess = (function() {
|
||||
var message = request.result;
|
||||
var type = message.type;
|
||||
var delivery = message.delivery;
|
||||
|
||||
var isIncoming = delivery === 'received' ||
|
||||
delivery === 'not-downloaded';
|
||||
|
||||
// Fill in the description/status/size
|
||||
if (type === 'sms') {
|
||||
setL10nAttributes(this.type, 'message-type-sms');
|
||||
} else { //mms
|
||||
setL10nAttributes(this.type, 'message-type-mms');
|
||||
// subject text content
|
||||
var subject = message.subject;
|
||||
|
||||
this.subject.classList.toggle('hide', !subject);
|
||||
if (subject) {
|
||||
this.subject.querySelector('.detail').textContent = subject;
|
||||
}
|
||||
|
||||
// Message total size show/hide
|
||||
if (message.attachments && message.attachments.length > 0) {
|
||||
var size = message.attachments.reduce(function(size, attachment) {
|
||||
return (size += attachment.content.size);
|
||||
}, 0);
|
||||
var params = Utils.getSizeForL10n(size);
|
||||
setL10nAttributes(this.size, params.l10nId, params.l10nArgs);
|
||||
this.sizeBlock.classList.remove('hide');
|
||||
}
|
||||
}
|
||||
this.container.dataset.delivery = delivery;
|
||||
this.container.classList.toggle('incoming-message', isIncoming);
|
||||
this.container.classList.toggle('outgoing-message', !isIncoming);
|
||||
|
||||
// If incoming message is migrated from the database where sentTimestamp
|
||||
// hadn't been supported yet then we won't have valid value for it.
|
||||
this.container.classList.toggle(
|
||||
'no-valid-sent-timestamp',
|
||||
isIncoming && !message.sentTimestamp
|
||||
);
|
||||
|
||||
setL10nAttributes(
|
||||
this.contactTitle,
|
||||
isIncoming ? 'report-from-title' : 'report-to-title'
|
||||
);
|
||||
|
||||
if (isIncoming) {
|
||||
l10nContainsDateSetup(this.receivedTimestamp, message.timestamp);
|
||||
l10nContainsDateSetup(this.sentTimestamp, message.sentTimestamp);
|
||||
setL10nAttributes(this.sentTitle, 'message-sent');
|
||||
this.renderLateArrivalMsg(message);
|
||||
|
||||
} else {
|
||||
if (delivery === 'sending' || delivery === 'sent') {
|
||||
setL10nAttributes(this.sentTitle, 'message-' + delivery);
|
||||
}
|
||||
if (delivery === 'error' || delivery === 'sent') {
|
||||
l10nContainsDateSetup(this.sentTimestamp, message.timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
//show sim information for dual sim device
|
||||
showSimInfo(this.simInfo, message.iccId);
|
||||
|
||||
// Filled in the contact list. Only outgoing message contains detailed
|
||||
// report information.
|
||||
this.renderContactList(createListWithMsgInfo(message));
|
||||
}).bind(this);
|
||||
},
|
||||
|
||||
renderLateArrivalMsg: function report_renderLateArrivalMsg(message) {
|
||||
var latenessBlock = this.panel.querySelector('.lateness-block');
|
||||
var lateness = message.timestamp - message.sentTimestamp;
|
||||
var showLatenessNotice = message.timestamp &&
|
||||
message.sentTimestamp &&
|
||||
lateness >= LATENESS_DISPLAY_LOWER_LIMIT;
|
||||
|
||||
if (showLatenessNotice) {
|
||||
var units = [
|
||||
{ unit: 'month', unitVal: MONTH },
|
||||
{ unit: 'day', unitVal: DAY },
|
||||
{ unit: 'hour', unitVal: HOUR },
|
||||
{ unit: 'minute', unitVal: MINUTE },
|
||||
{ unit: 'second', unitVal: SECOND }
|
||||
];
|
||||
|
||||
units.forEach(({unit, unitVal}) => {
|
||||
var output;
|
||||
var unitElement = this.panel.querySelector(`.lateness-${unit}`);
|
||||
|
||||
if (lateness >= unitVal) {
|
||||
// Compute integer value for the current unit output
|
||||
// and get the rest for the next smaller unit
|
||||
output = Math.floor(lateness / unitVal);
|
||||
lateness = lateness % unitVal;
|
||||
|
||||
document.l10n.setAttributes(
|
||||
unitElement,
|
||||
`duration-${unit}-narrow`,
|
||||
{ value: output }
|
||||
);
|
||||
unitElement.classList.remove('hide');
|
||||
} else {
|
||||
unitElement.classList.add('hide');
|
||||
}
|
||||
});
|
||||
latenessBlock.classList.remove('hide');
|
||||
} else {
|
||||
latenessBlock.classList.add('hide');
|
||||
}
|
||||
},
|
||||
|
||||
// Set this flag to true only when resend is triggered.
|
||||
messageResending: false,
|
||||
|
||||
setEventListener: function report_setEventListener() {
|
||||
this.resendBtn.addEventListener('click', () => {
|
||||
this.messageResending = true;
|
||||
ConversationView.resendMessage(this.id);
|
||||
});
|
||||
},
|
||||
|
||||
isReportForMessage: function report_isReportForMessage(id) {
|
||||
return Navigation.isCurrentPanel('report-view', { id: id }) ||
|
||||
(Navigation.isCurrentPanel('report-view') &&
|
||||
this.id === id);
|
||||
},
|
||||
|
||||
onStatusChanged: function report_onStatusChanged(e) {
|
||||
// If we got sending status change in report view after resend clicked
|
||||
// (messageResending is true), we should change report panel id, reset
|
||||
// messageResending flag and refresh for new message report.
|
||||
if (e.message.delivery === 'sending' && this.messageResending) {
|
||||
this.id = e.message.id;
|
||||
this.messageResending = false;
|
||||
}
|
||||
|
||||
this.isReportForMessage(e.message.id) && this.refresh();
|
||||
},
|
||||
|
||||
elements: ['contact-list', 'size', 'size-block', 'type', 'sent-title',
|
||||
'sent-timestamp', 'received-timestamp', 'subject', 'sim-info',
|
||||
'contact-title', 'resend-btn', 'header', 'container'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var Information = function(type) {
|
||||
Object.assign(this, VIEWS[type]);
|
||||
|
||||
if (this.init) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
this.panel = document.querySelector(this.selector);
|
||||
|
||||
this.elements.forEach(function(name) {
|
||||
this[Utils.camelCase(name)] = this.panel.querySelector('.' + name);
|
||||
}, this);
|
||||
|
||||
this.header.addEventListener(
|
||||
'action', this.backOrClose.bind(this)
|
||||
);
|
||||
|
||||
this.setEventListener && this.setEventListener();
|
||||
this.reset();
|
||||
};
|
||||
|
||||
Information.prototype = {
|
||||
constructor: Information,
|
||||
|
||||
beforeEnter: function(args) {
|
||||
this.id = +args.id;
|
||||
this.header.removeAttribute('no-font-fit');
|
||||
this.show();
|
||||
},
|
||||
|
||||
afterLeave: function() {
|
||||
this.reset();
|
||||
this.id = null;
|
||||
},
|
||||
|
||||
show: function() {
|
||||
this.render();
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
if (Navigation.isCurrentPanel(this.name)) {
|
||||
this.render();
|
||||
}
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
// Remove all LIs
|
||||
if (this.contactList) {
|
||||
this.contactList.textContent = '';
|
||||
}
|
||||
},
|
||||
|
||||
backOrClose: function() {
|
||||
Navigation.back();
|
||||
},
|
||||
|
||||
// Incrementing ID for each rendering request to avoid possible race when next
|
||||
// renderContactList request earlier than list item appended to contact list
|
||||
// ul. Ignoring the rendering/appending request if the rendering ID doesn't
|
||||
// match the latest rendering ID.
|
||||
renderingId: 0,
|
||||
|
||||
// Param participants could be:
|
||||
// - Array of contact number string or
|
||||
// - Array of object({ number: number, infoBlock: infoBlock })
|
||||
// for rendering the contact list.
|
||||
renderContactList: function(participants) {
|
||||
var ul = this.contactList;
|
||||
var renderer = ContactRenderer.flavor(this.contactFlavor);
|
||||
var currentRenderingId = ++this.renderingId;
|
||||
|
||||
ul.textContent = '';
|
||||
participants.forEach((participant) => {
|
||||
var number, infoBlock, selector;
|
||||
|
||||
if (typeof participant === 'object') {
|
||||
number = participant.number;
|
||||
infoBlock = participant.infoBlock;
|
||||
selector = '.js-contact-info';
|
||||
} else {
|
||||
number = participant;
|
||||
}
|
||||
Contacts.findByAddress(number).then((contacts) => {
|
||||
// If the current rendering ID doesn't match the latest ID, skip current
|
||||
// one and only render for ID which matches latest request ID.
|
||||
if (currentRenderingId !== this.renderingId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (contacts.length > 0) {
|
||||
renderer.render({
|
||||
contact: contacts[0],
|
||||
input: number,
|
||||
infoBlock: infoBlock,
|
||||
infoBlockParentSelector: selector,
|
||||
target: ul
|
||||
});
|
||||
} else {
|
||||
var li = document.createElement('li');
|
||||
li.role = 'presentation';
|
||||
li.innerHTML = TMPL.number.interpolate({
|
||||
number: number
|
||||
});
|
||||
|
||||
var parentBlock = li.querySelector(selector);
|
||||
if (parentBlock && infoBlock) {
|
||||
parentBlock.appendChild(infoBlock);
|
||||
}
|
||||
ul.appendChild(li);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Information.initDefaultViews = function() {
|
||||
// Create group / report information view
|
||||
exports.GroupView = new Information('group');
|
||||
exports.ReportView = new Information('report');
|
||||
};
|
||||
|
||||
exports.Information = Information;
|
||||
|
||||
// end global closure
|
||||
}(this));
|
||||
|
||||
49
gaia/apps/sms/views/conversation/js/link_action_handler.js
Executable file
49
gaia/apps/sms/views/conversation/js/link_action_handler.js
Executable file
|
|
@ -0,0 +1,49 @@
|
|||
/*global ActivityPicker, ConversationView */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
/*
|
||||
Centralized event handling for various
|
||||
data-actions url, email, phone in a message
|
||||
*/
|
||||
|
||||
var LinkActionHandler = {
|
||||
onClick: function lah_onClick(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
var dataset = event.target.dataset;
|
||||
var action = dataset.action;
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === 'email-link') {
|
||||
ConversationView.prompt({
|
||||
email: dataset.email,
|
||||
inMessage: true
|
||||
});
|
||||
}
|
||||
|
||||
if (action === 'dial-link') {
|
||||
ConversationView.promptContact({
|
||||
number: dataset.dial,
|
||||
inMessage: true
|
||||
});
|
||||
}
|
||||
|
||||
if (action === 'url-link') {
|
||||
|
||||
var type = action.replace('-link', '');
|
||||
|
||||
ActivityPicker[type](
|
||||
dataset[type], this.reset, this.reset
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
exports.LinkActionHandler = LinkActionHandler;
|
||||
}(this));
|
||||
301
gaia/apps/sms/views/conversation/js/link_helper.js
Executable file
301
gaia/apps/sms/views/conversation/js/link_helper.js
Executable file
|
|
@ -0,0 +1,301 @@
|
|||
/*global Utils */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
/*
|
||||
The LinkHelper exposes functionality to apply the regexp
|
||||
on the input strings and replace them with corresponding
|
||||
anchor links for url, phone, email.
|
||||
*/
|
||||
|
||||
var KNOWN_TLDS = [
|
||||
'aero', 'asia', 'biz', 'cat', 'com', 'coop', 'info',
|
||||
'int', 'jobs', 'mobi', 'museum', 'name', 'net', 'org', 'post', 'pro',
|
||||
'tel', 'travel', 'xxx', 'edu', 'gov', 'mil', 'nyc', 'ac',
|
||||
'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar',
|
||||
'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf',
|
||||
'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
|
||||
'no', 'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci',
|
||||
'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv', 'cx', 'cy',
|
||||
'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg',
|
||||
'eh', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr',
|
||||
'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn',
|
||||
'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn',
|
||||
'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir',
|
||||
'is', 'it', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km',
|
||||
'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk',
|
||||
'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg',
|
||||
'mh', 'mk', 'ml', 'mm', 'mn', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms',
|
||||
'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
|
||||
'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe',
|
||||
'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw',
|
||||
'py', 'qa', 're', 'ro', 'rs', 'ru', 'su', 'рф', 'rw', 'sa', 'sb',
|
||||
'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'no', 'sk', 'sl', 'sm',
|
||||
'sn', 'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc',
|
||||
'td', 'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tp', 'tm', 'tn', 'to',
|
||||
'tp', 'tl', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug', 'uk', 'us',
|
||||
'gov', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
|
||||
'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zw'
|
||||
];
|
||||
|
||||
var ipv4part = '(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)';
|
||||
var ipv4RegExp = new RegExp('^(?:' + ipv4part + '\\.){3}' + ipv4part + '$');
|
||||
var leadingDigitsRegExp = /^[+0-9]+/;
|
||||
|
||||
// ensure that each part of the domain is long enough
|
||||
function checkDomain(domain) {
|
||||
// Check for a specific IPv4 address
|
||||
if (ipv4RegExp.test(domain)) {
|
||||
return true;
|
||||
} else {
|
||||
// Don't add many restrictions,
|
||||
// just the tld to be non numeric and length > 1
|
||||
var parts = domain.split('.');
|
||||
var lastPart = parts[parts.length - 1];
|
||||
// We want the last part not to be a number
|
||||
return lastPart.length > 1 && !isFinite(lastPart);
|
||||
}
|
||||
}
|
||||
|
||||
// defines things that can match right before to be a "safe" link
|
||||
var safeStart = /[\s.,:;\(>\u0080-\uFFFF]/;
|
||||
|
||||
const MINIMUM_DIGITS_IN_PHONE_NUMBER = 5;
|
||||
const LEADING_DIGIT_BREAKPOINT = 7;
|
||||
|
||||
/**
|
||||
* For each category of links:
|
||||
* The key is the link type
|
||||
* order matters - defined first means higher priority if two matches equal
|
||||
*
|
||||
* regexp: The regular expression to match potential links
|
||||
* matchFilter: (optional) A function that is passed the matched part, and
|
||||
* the linkSpec.
|
||||
* Should return the linkSpec, or false if not to be matched
|
||||
* transform: A function that converts the match data to a html link string
|
||||
* Is passed the matched part and the linkSpec.
|
||||
*/
|
||||
var LINK_TYPES = {
|
||||
phone: {
|
||||
regexp: new RegExp([
|
||||
// sddp: space, dot, dash or parens
|
||||
'(?:\\+\\d{1,4}[ \\t.()-]{0,3}|\\()?' + // (\+<digits><sddp>|\()?
|
||||
'(?:\\d{1,4}[ \\t.()-]{0,3})?' + // <digits><sdd>*
|
||||
'(?:\\d[\\d \\t.()-]{0,12}\\d)' + // <digit><digit|sddp>*<digit>
|
||||
'\\b' // must end on a word boundary
|
||||
].join(''), 'g'),
|
||||
matchFilter: function phoneMatchFilter(phone, link) {
|
||||
var onlyDigits = Utils.removeNonDialables(phone);
|
||||
|
||||
if (onlyDigits.length < MINIMUM_DIGITS_IN_PHONE_NUMBER) {
|
||||
return false;
|
||||
}
|
||||
if (onlyDigits.length === MINIMUM_DIGITS_IN_PHONE_NUMBER &&
|
||||
phone.length !== onlyDigits.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var leadingDigits = leadingDigitsRegExp.exec(phone);
|
||||
if (leadingDigits &&
|
||||
leadingDigits[0].length >= LEADING_DIGIT_BREAKPOINT) {
|
||||
link.end = link.start + leadingDigits[0].length;
|
||||
}
|
||||
return link;
|
||||
},
|
||||
transform: function phoneTransform(phone, link) {
|
||||
return '<a dir="ltr" data-dial="' + phone +
|
||||
'" data-action="dial-link">' + phone + '</a>';
|
||||
}
|
||||
},
|
||||
|
||||
url: {
|
||||
regexp: new RegExp([
|
||||
// must begin at start of string, after whitespace,
|
||||
// {1} match the protocol https?:// or rtsp:// (optional)
|
||||
'(https?://|rtsp://)?',
|
||||
// {2} match "server name": . must be followed by at least one letter
|
||||
'(\\w(?:\\.?[-\\w]){0,255})',
|
||||
// {3} match a . followed by one or more domain valid chars
|
||||
'(\\.\\w{1,10})',
|
||||
// optional :port
|
||||
'(?::[0-9]{1,5})?',
|
||||
// start the "query" capture group by matching an optional dot then /
|
||||
'(?:\\.?/',
|
||||
// anything other than a sentence ender
|
||||
// a sentence ender not followed by whitespace
|
||||
// match 0 - 2048 characters in the url
|
||||
'(?:[^\\s.,?:;!]|[.,?:;!](?!\\s|$)){0,2048}',
|
||||
// end the "query" capture group (optional)
|
||||
')?'
|
||||
].join(''), 'mgi'),
|
||||
matchFilter: function urlMatchFilter(url, linkSpec) {
|
||||
var match = linkSpec.match;
|
||||
var scheme, tld;
|
||||
|
||||
if (!checkDomain(match[2] + match[3])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
scheme = match[1];
|
||||
tld = match[3] && match[3].slice(1);
|
||||
|
||||
// For Cases where:
|
||||
//
|
||||
// 1. There was no scheme (eg, "http", "https", "rtsp")
|
||||
// 2. The matched tld is not a number
|
||||
// 3. The matched tld is not a known tld
|
||||
//
|
||||
// ... Do not create a clickable link
|
||||
if (!scheme && (!isFinite(tld) && KNOWN_TLDS.indexOf(tld) === -1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// strip a trailing ) if there isn't a ( in the url
|
||||
if (url.slice(-1) === ')' && url.indexOf('(') === -1) {
|
||||
linkSpec.end--;
|
||||
}
|
||||
return linkSpec;
|
||||
},
|
||||
transform: function urlTransform(url, linkSpec) {
|
||||
var href = url;
|
||||
if (!linkSpec.match[1]) {
|
||||
href = 'http://' + href;
|
||||
}
|
||||
return '<a data-url="' + href + '" data-action="url-link" >' + url +
|
||||
'</a>';
|
||||
}
|
||||
},
|
||||
|
||||
email: {
|
||||
regexp: /[\w.+-]+@[\w.-]+\.[a-z.]{2,6}/mgi,
|
||||
transform: function emailTransform(email) {
|
||||
return [
|
||||
'<a data-email="',
|
||||
'" data-action="email-link">',
|
||||
'</a>'
|
||||
].join(email);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var LINK_TYPES_KEYS = Object.keys(LINK_TYPES);
|
||||
|
||||
function searchForLinks(type, string) {
|
||||
var linkSpecs = [];
|
||||
var spec = LINK_TYPES[type];
|
||||
|
||||
if (!spec) {
|
||||
return linkSpecs;
|
||||
}
|
||||
|
||||
var regexp = spec.regexp;
|
||||
var matchFilter = spec.matchFilter;
|
||||
var match, linkSpec;
|
||||
|
||||
// while we match stuff...
|
||||
while ((match = regexp.exec(string))) {
|
||||
|
||||
// if the match isn't at the begining of the string, check for a safe
|
||||
// character before the match
|
||||
|
||||
var rest = string.slice(match.index - 1);
|
||||
if (match.index && !safeStart.test(rest.charAt(0))) {
|
||||
|
||||
// we should only advance the regexp to the next safeStart
|
||||
var nextSafe = safeStart.exec(rest);
|
||||
if (nextSafe) {
|
||||
regexp.lastIndex = match.index + nextSafe.index;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
linkSpec = {
|
||||
type: type,
|
||||
start: match.index,
|
||||
length: match[0].length,
|
||||
end: match.index + match[0].length,
|
||||
match: match
|
||||
};
|
||||
|
||||
if (matchFilter) {
|
||||
linkSpec = matchFilter(match[0], linkSpec);
|
||||
}
|
||||
|
||||
if (linkSpec) {
|
||||
linkSpecs.push(linkSpec);
|
||||
regexp.lastIndex = linkSpec.end;
|
||||
}
|
||||
}
|
||||
|
||||
return linkSpecs;
|
||||
}
|
||||
|
||||
function linkSort(a, b) {
|
||||
// sort by starting position first
|
||||
// then by ending position reversed
|
||||
// then by the order of the type in the LINK_TYPES
|
||||
return (a.start - b.start) ||
|
||||
(b.end - a.end) ||
|
||||
(LINK_TYPES_KEYS.indexOf(a.type) - LINK_TYPES_KEYS.indexOf(b.type));
|
||||
}
|
||||
|
||||
function removeOverlapping(linkSpecs) {
|
||||
linkSpecs.sort(linkSort);
|
||||
for (var index = 0; index < linkSpecs.length - 1; index++) {
|
||||
var end = linkSpecs[index].end;
|
||||
// while there are more linkSpecs, and they start before we end remove the
|
||||
// overlapping matches
|
||||
while (linkSpecs[index + 1] && linkSpecs[index + 1].start < end) {
|
||||
linkSpecs.splice(index + 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var LinkHelper = window.LinkHelper = {
|
||||
searchAndLinkUrl: function lh_searchAndLinkUrl(inputText) {
|
||||
return LinkHelper.searchAndLinkClickableData(inputText, { url: true });
|
||||
},
|
||||
searchAndLinkEmail: function lh_searchAndLinkEmail(inputText) {
|
||||
return LinkHelper.searchAndLinkClickableData(inputText, { email: true });
|
||||
},
|
||||
searchAndLinkPhone: function lh_searchAndLinkPhone(inputText) {
|
||||
return LinkHelper.searchAndLinkClickableData(inputText, { phone: true });
|
||||
},
|
||||
// Invokes resepective functions to change url, phone and email strings
|
||||
// and make them active links
|
||||
// |inputText| should be already html escaped
|
||||
searchAndLinkClickableData:
|
||||
function lh_searchAndLinkClickableData(inputText, mode) {
|
||||
// default is everything enabled
|
||||
mode = mode || LINK_TYPES;
|
||||
|
||||
var linkSpecs = [];
|
||||
var type;
|
||||
|
||||
for (type in mode) {
|
||||
if (mode[type] && LINK_TYPES[type]) {
|
||||
linkSpecs = linkSpecs.concat(searchForLinks(type, inputText));
|
||||
}
|
||||
}
|
||||
|
||||
removeOverlapping(linkSpecs);
|
||||
|
||||
var result = '';
|
||||
var lastEnd = 0;
|
||||
|
||||
linkSpecs.forEach(function replaceLink(link) {
|
||||
result += inputText.slice(lastEnd, link.start);
|
||||
|
||||
var replacing = inputText.slice(link.start, link.end);
|
||||
var replaceWith = LINK_TYPES[link.type].transform(replacing, link);
|
||||
result += replaceWith;
|
||||
lastEnd = link.end;
|
||||
});
|
||||
|
||||
result += inputText.slice(lastEnd);
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
1076
gaia/apps/sms/views/conversation/js/recipients.js
Executable file
1076
gaia/apps/sms/views/conversation/js/recipients.js
Executable file
File diff suppressed because it is too large
Load diff
87
gaia/apps/sms/views/conversation/js/startup.js
Executable file
87
gaia/apps/sms/views/conversation/js/startup.js
Executable file
|
|
@ -0,0 +1,87 @@
|
|||
/* global App,
|
||||
ConversationView,
|
||||
Information,
|
||||
InterInstanceEventDispatcher,
|
||||
LazyLoader,
|
||||
LocalizationHelper,
|
||||
MessageManager,
|
||||
MessagingClient,
|
||||
MozMobileConnectionsClient,
|
||||
Navigation,
|
||||
Settings,
|
||||
Threads,
|
||||
TimeHeaders,
|
||||
Utils
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const LAZY_DEPENDENCIES = [
|
||||
'/services/js/messaging/messaging_client.js',
|
||||
'/services/js/moz_mobile_connections/moz_mobile_connections_client.js',
|
||||
'/shared/js/settings_listener.js',
|
||||
'/shared/js/mime_mapper.js',
|
||||
'/shared/js/option_menu.js',
|
||||
'/shared/js/mobile_operator.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_action_handler.js',
|
||||
'/views/shared/js/contact_renderer.js',
|
||||
'/views/shared/js/activity_picker.js',
|
||||
'/views/conversation/js/information.js',
|
||||
'/views/shared/js/localization_helper.js'
|
||||
];
|
||||
|
||||
function initLazyDependencies() {
|
||||
return LazyLoader.load(LAZY_DEPENDENCIES).then(() => {
|
||||
LocalizationHelper.init();
|
||||
TimeHeaders.init();
|
||||
Information.initDefaultViews();
|
||||
Settings.init();
|
||||
MessagingClient.init(App.instanceId);
|
||||
MozMobileConnectionsClient.init(App.instanceId);
|
||||
Navigation.setReady();
|
||||
|
||||
InterInstanceEventDispatcher.connect();
|
||||
});
|
||||
}
|
||||
|
||||
function initShims() {
|
||||
exports.InboxView = Object.freeze({
|
||||
markReadUnread: () => {},
|
||||
updateThread: () => {}
|
||||
});
|
||||
|
||||
var deferred = Utils.Promise.defer();
|
||||
|
||||
MessageManager.getThreads({
|
||||
each: (thread) => Threads.set(thread.id, thread),
|
||||
done: deferred.resolve
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
exports.Startup = {
|
||||
init() {
|
||||
Utils.initializeShimHost(App.instanceId);
|
||||
|
||||
MessageManager.init();
|
||||
ConversationView.init();
|
||||
if (Navigation.isDefaultPanel()) {
|
||||
ConversationView.once('visually-loaded', initLazyDependencies);
|
||||
} else {
|
||||
initLazyDependencies();
|
||||
}
|
||||
|
||||
// Note that we won't be able to access report and group views directly
|
||||
// because information.js is not loaded before ConversationView finishes
|
||||
// loading.
|
||||
initShims().then(() => Navigation.init());
|
||||
}
|
||||
};
|
||||
})(window);
|
||||
178
gaia/apps/sms/views/conversation/js/subject_composer.js
Executable file
178
gaia/apps/sms/views/conversation/js/subject_composer.js
Executable file
|
|
@ -0,0 +1,178 @@
|
|||
/* global EventDispatcher */
|
||||
|
||||
/* exported SubjectComposer */
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const MAX_LENGTH = 40;
|
||||
const privateMembers = new WeakMap();
|
||||
|
||||
function onKeyDown(e) {
|
||||
/* jshint validthis: true */
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
if (e.keyCode === e.DOM_VK_BACK_SPACE) {
|
||||
if (!priv.isHoldingBackspace) {
|
||||
priv.isEmptyOnBackspace = !this.getValue();
|
||||
priv.isHoldingBackspace = true;
|
||||
}
|
||||
} else {
|
||||
priv.isHoldingBackspace = false;
|
||||
// Input char will be ignored when:
|
||||
// - Reaching the maximum subject length. Any char input is not allowed
|
||||
// - Return key(new line) input. Since new line won't work in subject
|
||||
if (this.getValue().length >= MAX_LENGTH ||
|
||||
e.keyCode === e.DOM_VK_RETURN) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyUp(e) {
|
||||
/* jshint validthis: true */
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
// User removes subject field by either:
|
||||
// - Selecting options menu in top right hand corner, or
|
||||
// - Selecting the delete button in the keyboard. if all
|
||||
// the text is removed from the subject field and the user
|
||||
// selects delete on the keyboard the subject field is removed
|
||||
if (e.keyCode === e.DOM_VK_BACK_SPACE && priv.isHoldingBackspace &&
|
||||
priv.isEmptyOnBackspace) {
|
||||
this.hide();
|
||||
}
|
||||
priv.isEmptyOnBackspace = false;
|
||||
priv.isHoldingBackspace = false;
|
||||
}
|
||||
|
||||
function onInput(e) {
|
||||
/* jshint validthis: true */
|
||||
privateMembers.get(this).updateValue(e.target.textContent);
|
||||
}
|
||||
|
||||
function updatePlaceholder() {
|
||||
/* jshint validthis: true */
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
if (priv.value) {
|
||||
priv.placeholder.remove();
|
||||
} else {
|
||||
priv.node.appendChild(priv.placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
function updateValue(newValue) {
|
||||
/* jshint validthis: true */
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
// Bug 877141 - contenteditable will insert non-break spaces when
|
||||
// multiple consecutive spaces are entered, we don't want them.
|
||||
if (newValue) {
|
||||
newValue = newValue.replace(/\n\s*|\u00A0/g, ' ');
|
||||
}
|
||||
|
||||
if (priv.value !== newValue) {
|
||||
priv.value = newValue;
|
||||
priv.updatePlaceholder();
|
||||
|
||||
this.emit('change');
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
var SubjectComposer = function(node) {
|
||||
EventDispatcher.mixin(this, [
|
||||
'focus',
|
||||
'change',
|
||||
'visibility-change'
|
||||
]);
|
||||
|
||||
if (!node) {
|
||||
throw new Error('Subject node is required');
|
||||
}
|
||||
|
||||
var priv = {
|
||||
isHoldingBackspace: false,
|
||||
isEmptyOnBackspace: false,
|
||||
isVisible: false,
|
||||
value: '',
|
||||
|
||||
// nodes
|
||||
node: node,
|
||||
input: null,
|
||||
placeholder: null,
|
||||
|
||||
// methods
|
||||
updateValue: updateValue.bind(this),
|
||||
updatePlaceholder: updatePlaceholder.bind(this),
|
||||
};
|
||||
privateMembers.set(this, priv);
|
||||
|
||||
priv.input = priv.node.querySelector('.subject-composer-input');
|
||||
priv.placeholder = priv.node.querySelector(
|
||||
'.subject-composer-placeholder'
|
||||
);
|
||||
|
||||
priv.input.addEventListener('keydown', onKeyDown.bind(this));
|
||||
priv.input.addEventListener('keyup', onKeyUp.bind(this));
|
||||
priv.input.addEventListener('input', onInput.bind(this));
|
||||
priv.input.addEventListener('focus', this.emit.bind(this, 'focus'));
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.show = function ms_show() {
|
||||
this.toggle(true);
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.hide = function ms_hide() {
|
||||
this.toggle(false);
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.toggle = function ms_toggle(toggle) {
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
priv.node.classList.toggle('hide', !toggle);
|
||||
priv.isVisible = toggle;
|
||||
|
||||
this.emit('visibility-change');
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.isVisible = function ms_is_visible() {
|
||||
return privateMembers.get(this).isVisible;
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.focus = function ms_focus() {
|
||||
privateMembers.get(this).input.focus();
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.getMaxLength = function ms_get_maxLength() {
|
||||
return MAX_LENGTH;
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.getValue = function ms_get_value() {
|
||||
return privateMembers.get(this).value;
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.setValue = function ms_set_value(value) {
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error('Value should be a valid string!');
|
||||
}
|
||||
|
||||
priv.input.textContent = priv.updateValue(value);
|
||||
};
|
||||
|
||||
SubjectComposer.prototype.reset = function ms_reset() {
|
||||
var priv = privateMembers.get(this);
|
||||
|
||||
priv.value = priv.input.textContent = '';
|
||||
priv.node.classList.add('hide');
|
||||
priv.isVisible = false;
|
||||
|
||||
priv.updatePlaceholder();
|
||||
};
|
||||
|
||||
exports.SubjectComposer = SubjectComposer;
|
||||
})(window);
|
||||
Loading…
Reference in a new issue