/*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));