Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
582 lines
17 KiB
JavaScript
Executable file
582 lines
17 KiB
JavaScript
Executable file
/*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));
|
|
|