VulpesOS/gaia/apps/sms/views/conversation/js/information.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

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