VulpesOS/gaia/apps/sms/views/inbox/js/inbox.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

1076 lines
31 KiB
JavaScript
Executable file

/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/*global Template, Utils, Threads, Contacts, Threads,
WaitingScreen, MessageManager, TimeHeaders,
Drafts, Thread, OptionMenu, ActivityPicker,
StickyHeader, Navigation,
SelectionHandler,
Settings,
LazyLoader,
EventDispatcher
*/
/*exported InboxView */
(function(exports) {
'use strict';
const privateMembers = new WeakMap();
function createBdiNode(content) {
var bdi = document.createElement('bdi');
bdi.textContent = content;
return bdi;
}
var InboxView = {
DRAFT_SAVED_DURATION: 5000,
FIRST_PANEL_THREAD_COUNT: 9, // counted on a Peak
// Used to track timeouts
timeouts: {
onDraftSaved: null
},
// Used to track the current number of rendered
// threads. Updated in InboxView.renderThreads
count: 0,
// Set to |true| when in edit mode
inEditMode: false,
/**
* Indicates that draft with particular id has been saved recently and user
* should be notified about it.
*/
notifyAboutSavedDraftWithId: null,
init: function inbox_init() {
this.tmpl = {
thread: Template('messages-thread-tmpl')
};
// TODO: https://bugzilla.mozilla.org/show_bug.cgi?id=854413
[
'container', 'no-messages', 'read-unread-button',
'check-uncheck-all-button','composer-link',
'delete-button', 'edit-header','options-button',
'settings-button','edit-mode', 'edit-form',
'draft-saved-banner'
].forEach(function(id) {
this[Utils.camelCase(id)] = document.getElementById('threads-' + id);
}, this);
this.mainWrapper = document.getElementById('main-wrapper');
// TODO this should probably move to a "WrapperView" class
this.composerLink.addEventListener(
'click', this.launchComposer.bind(this)
);
this.readUnreadButton.addEventListener('click', () => {
this.markReadUnread(
this.selectionHandler.selectedList,
this.readUnreadButton.dataset.action === 'mark-as-read'
);
});
this.deleteButton.addEventListener('click', () => {
this.delete(this.selectionHandler.selectedList);
});
this.editHeader.addEventListener(
'action', this.cancelEdit.bind(this)
);
this.optionsButton.addEventListener(
'click', this.showOptions.bind(this)
);
this.settingsButton.addEventListener(
'click', function oSettings() {
ActivityPicker.openSettings();
}
);
this.container.addEventListener(
'click', this
);
this.container.addEventListener(
'contextmenu', this
);
this.editForm.addEventListener(
'submit', this
);
navigator.mozContacts.addEventListener(
'contactchange',
this.updateContactsInfo.bind(this)
);
MessageManager.on('message-sending', this.onMessageSending.bind(this));
MessageManager.on('message-received', this.onMessageReceived.bind(this));
MessageManager.on('threads-deleted', this.onThreadsDeleted.bind(this));
Drafts.on('deleted', this.onDraftDeleted.bind(this));
Drafts.on('saved', this.onDraftSaved.bind(this));
privateMembers.set(this, {
// Very approximate number of letters that can fit into title for the
// group thread, "100" is for all paddings, image width and so on,
// 10 is approximate English char width for current 18px font size
groupThreadTitleMaxLength: (window.innerWidth - 100) / 10
});
this.once('fully-loaded', () => {
this.ensureReadAheadSetting();
});
this.once('visually-loaded', () => {
this.initStickyHeader();
});
this.sticky = null;
},
initStickyHeader: function inbox_initStickyHeader() {
if (!this.sticky) {
this.sticky =
new StickyHeader(this.container, document.getElementById('sticky'));
}
},
beforeEnter: function inbox_beforeEnter(args = {}) {
// In case user saved draft when Inbox was not the active view, we want to
// notify that save operation successfully completed once user returns back
// to Inbox view.
if (this.notifyAboutSavedDraftWithId) {
this.showDraftSavedBanner();
this.notifyAboutSavedDraftWithId = null;
}
},
beforeLeave: function inbox_beforeLeave() {
// This should be in afterLeave, but the edit mode interface does not seem
// to slide correctly. Bug 1009541
this.cancelEdit();
},
getIdIterator: function inbox_getIdIterator() {
return Threads.keys();
},
setContact: function inbox_setContact(node) {
var threadId = node.dataset.threadId;
var draftId = node.dataset.draftId;
var threadOrDraft = draftId ?
Drafts.byDraftId(+draftId) : Threads.get(+threadId);
if (!threadOrDraft) {
throw new Error('Thread node is invalid!');
}
var threadNumbers = threadOrDraft.participants || threadOrDraft.recipients;
var titleContainer = node.querySelector('.threadlist-item-title');
var title = titleContainer.firstElementChild;
var picture = node.querySelector('.threadlist-item-picture');
if (!threadNumbers || !threadNumbers.length) {
title.setAttribute('data-l10n-id', 'no-recipient');
return;
}
function* updateThreadNode(number) {
var contact = yield InboxView.findContact(number, { photoURL: true });
var isContact = !!contact.isContact;
picture.classList.toggle('has-picture', isContact);
picture.classList.toggle(
'default-picture', isContact && !contact.photoURL
);
title.textContent = contact.title || number;
var photoUrl = node.dataset.photoUrl;
if (photoUrl) {
window.URL.revokeObjectURL(photoUrl);
}
if (contact.photoURL) {
node.dataset.photoUrl = contact.photoURL;
} else if (photoUrl) {
node.dataset.photoUrl = '';
}
if (contact.photoURL) {
// Use multiple image background to display default image until real
// contact image thumbnail is decoded by Gecko. Difference is especially
// noticeable on slow devices. Please keep default image in sync with
// what defined in CSS (sms.css/.threadlist-item-picture)
picture.firstElementChild.style.backgroundImage = [
'url(' + contact.photoURL + ')',
'url(/views/inbox/style/images/default_contact_image.png)'
].join(', ');
} else {
picture.firstElementChild.style.backgroundImage = null;
}
}
function* updateGroupThreadNode(numbers, titleMaxLength) {
var contactTitle, number;
var i = 0;
var threadTitleLength = 0;
var groupTitle = document.createElement('span');
var separatorNode = document.createElement('span');
separatorNode.setAttribute(
'data-l10n-id',
'thread-participant-separator'
);
picture.firstElementChild.textContent = numbers.length;
picture.classList.add('has-picture', 'group-picture');
while (i < numbers.length && threadTitleLength < titleMaxLength) {
number = numbers[i++];
contactTitle = (yield InboxView.findContact(number)).title || number;
if (threadTitleLength > 0) {
groupTitle.appendChild(separatorNode.cloneNode(true));
}
groupTitle.appendChild(createBdiNode(contactTitle));
threadTitleLength += contactTitle.length;
}
titleContainer.replaceChild(groupTitle, title);
}
if (!title.textContent) {
// display something before waiting for findContact's result
title.textContent = threadNumbers[0];
}
if (threadNumbers.length === 1) {
return Utils.Promise.async(updateThreadNode)(threadNumbers[0]);
}
return Utils.Promise.async(updateGroupThreadNode)(
threadNumbers, privateMembers.get(this).groupThreadTitleMaxLength
);
},
findContact: function(number, options) {
return Contacts.findByAddress(number).then(function(contacts) {
var details = Utils.getContactDetails(number, contacts, options);
if (!details.isContact) {
Contacts.addUnknown(number);
}
return details;
});
},
handleEvent: function inbox_handleEvent(event) {
var parent = event.target.parentNode;
var threadId = parent.dataset.threadId;
switch (event.type) {
case 'click':
// Handle selection in selection module
if (this.inEditMode) {
return;
}
if (threadId) {
event.preventDefault();
var draftId = parent.dataset.draftId;
if (draftId) {
Navigation.toPanel('composer', {
draftId: +draftId,
});
} else {
Navigation.toPanel('thread', {
id: +threadId
});
}
}
break;
case 'contextmenu':
if (this.inEditMode || !threadId) {
return;
}
event.preventDefault();
event.stopPropagation();
// Show options per single thread
var params = {
type: 'action',
header: { l10nId: 'thread-options' },
items: [{
l10nId: 'delete-thread',
method: this.delete.bind(this, [threadId])
}]
};
var thread = Threads.get(+threadId);
if (!thread.isDraft) {
var isRead = thread.unreadCount > 0;
var l10nKey = isRead ? 'mark-as-read' : 'mark-as-unread';
params.items.push(
{
l10nId: l10nKey,
method: this.markReadUnread.bind(this, [threadId], isRead)
}
);
}
params.items.push({
l10nId: 'cancel'
});
var options = new OptionMenu(params);
options.show();
break;
case 'submit':
event.preventDefault();
break;
}
},
launchComposer: function inbox_launchComposer(e) {
// prevent following the link, see also bug 1014219
e.preventDefault();
Navigation.toPanel('composer');
},
updateSelectionStatus: function inbox_updateSelectionStatus() {
var selected = this.selectionHandler;
if (this.selectionHandler.allSelected()) {
this.checkUncheckAllButton.setAttribute('data-l10n-id', 'deselect-all');
} else {
this.checkUncheckAllButton.setAttribute('data-l10n-id', 'select-all');
}
if (selected.selectedCount) {
this.deleteButton.disabled = false;
document.l10n.setAttributes(this.editMode, 'selected-threads', {
n: selected.selectedCount
});
var hasUnreadselected = selected.selectedList.some((id) => {
var thread = Threads.get(id);
if (thread && thread.unreadCount) {
return thread.unreadCount > 0;
}
return false;
});
var allDraft = selected.selectedList.every((id) => {
return (Threads.get(id).isDraft);
});
if (allDraft) {
this.readUnreadButton.disabled = true;
} else {
if (!hasUnreadselected) {
this.readUnreadButton.dataset.action = 'mark-as-unread';
} else {
this.readUnreadButton.dataset.action = 'mark-as-read';
}
this.readUnreadButton.disabled = false;
}
} else {
this.deleteButton.disabled = true;
this.readUnreadButton.disabled = true;
document.l10n.setAttributes(this.editMode, 'selectThreads-title');
}
},
markReadUnread: function inbox_markReadUnread(selected, isRead) {
selected.forEach((id) => {
var thread = Threads.get(+id);
var readStatusMatched = !!thread.unreadCount ^ isRead;
var markable = thread && !thread.isDraft &&
(isRead || !thread.getDraft());
if (markable && !readStatusMatched) {
thread.unreadCount = isRead ? 0 : 1;
this.mark(thread.id, isRead ? 'read' : 'unread');
MessageManager.markThreadRead(thread.id, isRead);
}
});
this.cancelEdit();
},
removeConversationDOM(conversationId) {
var li = document.getElementById('thread-' + conversationId);
var parent;
var photoUrl = li && li.dataset.photoUrl;
// Revoke the contact photo while deletion for avoiding intermittent
// photo disappear issue.
if (photoUrl) {
window.URL.revokeObjectURL(photoUrl);
}
if (li) {
parent = li.parentNode;
li.remove();
}
// remove the header and the ul for an empty list
if (parent && !parent.firstElementChild) {
parent.previousSibling.remove();
parent.remove();
this.sticky && this.sticky.refresh();
// if we have no more elements, set empty classes
if (!this.container.querySelector('li')) {
this.setEmpty(true);
}
}
},
// Since removeConversationDOM will revoke list photoUrl at the end of
// deletion, please make sure url will also be revoked if new delete api
// remove threads without calling removeConversationDOM in the future.
delete: function inbox_delete(selected) {
function performDeletion() {
/* jshint validthis: true */
var threadIdsToDelete = [],
messageIdsToDelete = [],
threadCountToDelete = 0;
function exitEditMode() {
InboxView.cancelEdit();
WaitingScreen.hide();
}
function onAllThreadMessagesRetrieved() {
if (!--threadCountToDelete) {
MessageManager.deleteMessages(messageIdsToDelete);
threadIdsToDelete.forEach(function(threadId) {
InboxView.deleteThread(threadId);
});
messageIdsToDelete = threadIdsToDelete = null;
exitEditMode();
}
}
function onThreadMessageRetrieved(message) {
messageIdsToDelete.push(message.id);
return true;
}
WaitingScreen.show();
threadIdsToDelete = selected.reduce(function(list, value) {
// Coerce the threadId back to a number MobileMessageFilter and all
// other platform APIs expect this value to be a number.
var threadId = +value;
if (Threads.get(threadId).isDraft) {
InboxView.deleteThread(threadId);
} else {
list.push(threadId);
}
return list;
}, []);
if (!threadIdsToDelete.length) {
exitEditMode();
return;
}
threadCountToDelete = threadIdsToDelete.length;
threadIdsToDelete.forEach(function(threadId) {
MessageManager.getMessages({
// Filter and request all messages with this threadId
filter: { threadId: threadId },
each: onThreadMessageRetrieved,
end: onAllThreadMessagesRetrieved
});
});
}
return Utils.confirm(
{
id: 'deleteThreads-confirmation-message',
args: { n: selected.length }
},
null,
{
text: 'delete',
className: 'danger'
}
).then(performDeletion.bind(this));
},
setEmpty: function inbox_setEmpty(empty) {
var panel = document.querySelector('.panel-InboxView');
// Hide the container when threadlist is empty.
panel.classList.toggle('threadlist-is-empty', !!empty);
},
showOptions: function inbox_options() {
var params = {
items: [{
l10nId: 'selectThreads-label',
method: this.startEdit.bind(this)
},{
l10nId: 'settings',
method: function oSettings() {
ActivityPicker.openSettings();
}
},{ // Last item is the Cancel button
l10nId: 'cancel',
incomplete: true
}]
};
new OptionMenu(params).show();
},
startEdit: function inbox_edit() {
function editModeSetup() {
/*jshint validthis:true */
this.inEditMode = true;
this.selectionHandler.cleanForm();
this.mainWrapper.classList.toggle('edit');
}
this.editHeader.removeAttribute('no-font-fit');
if (!this.selectionHandler) {
LazyLoader.load([
'/views/shared/js/selection_handler.js',
'/shared/style/edit_mode.css',
'/shared/style/switches.css',
'/views/shared/style/edit-mode.css',
'/shared/style/tabs.css',
'/views/inbox/style/edit-mode.css'
], () => {
this.selectionHandler = new SelectionHandler({
// Elements
container: this.container,
checkUncheckAllButton: this.checkUncheckAllButton,
// Methods
updateSelectionStatus: this.updateSelectionStatus.bind(this),
getIdIterator: this.getIdIterator.bind(this),
isInEditMode: this.isInEditMode.bind(this)
});
this.editForm.classList.remove('hide');
editModeSetup.call(this);
});
} else {
editModeSetup.call(this);
}
},
isInEditMode: function inbox_isInEditMode() {
return this.inEditMode;
},
cancelEdit: function inbox_cancelEdit() {
this.inEditMode = false;
this.mainWrapper.classList.remove('edit');
},
renderDrafts: function inbox_renderDrafts() {
// Request and render all threads with drafts
// or thread-less drafts.
return Drafts.request().then(() => {
for (var draft of Drafts.getAll()) {
if (draft.threadId) {
// Find draft-containing threads that have already been rendered
// and update them so they mark themselves appropriately
if (document.getElementById(`thread-${draft.threadId}`)) {
this.updateThread(
Threads.get(draft.threadId), { conversationDraft: true }
);
}
} else {
// Safely assume there is a threadless draft
this.setEmpty(false);
// Render draft only in case we haven't rendered it yet.
if (!Threads.has(draft.id)) {
var thread = Thread.create(draft);
Threads.set(draft.id, thread);
this.appendThread(thread);
}
}
}
this.sticky && this.sticky.refresh();
});
},
prepareRendering: function inbox_prepareRendering() {
this.container.innerHTML = '';
this.renderDrafts();
},
startRendering: function inbox_startRenderingThreads() {
this.setEmpty(false);
},
finalizeRendering: function inbox_finalizeRendering(empty) {
if (empty) {
this.setEmpty(true);
}
if (!empty) {
TimeHeaders.updateAll('header[data-time-update]');
}
this.sticky && this.sticky.refresh();
},
ensureReadAheadSetting: function inbox_ensureReadAheadSettting() {
Settings.setReadAheadThreadRetrieval(this.FIRST_PANEL_THREAD_COUNT);
},
renderThreads: function inbox_renderThreads() {
window.performance.mark('willRenderThreads');
var hasThreads = false;
var firstPanelCount = this.FIRST_PANEL_THREAD_COUNT;
this.prepareRendering();
function onRenderThread(thread) {
/* jshint validthis: true */
// Register all threads to the Threads object.
Threads.set(thread.id, thread);
if (!hasThreads) {
hasThreads = true;
this.startRendering();
}
this.appendThread(thread);
// Dispatch visually-loaded when rendered threads could fill up the top of
// the visible area.
if (--firstPanelCount === 0) {
this.emit('visually-loaded');
}
}
function onThreadsRendered() {
/* jshint validthis: true */
/* We set the view as empty only if there's no threads and no drafts,
* this is done to prevent races between renering threads and drafts. */
this.finalizeRendering(!(hasThreads || Drafts.size));
// Dispatch visually-loaded when rendered threads could fill up the top of
// the visible area.
if (firstPanelCount > 0) {
this.emit('visually-loaded');
}
}
MessageManager.getThreads({
each: onRenderThread.bind(this),
end: onThreadsRendered.bind(this),
// We should emit 'fully-loaded' event only when drafts are loaded and
// processed, Drafts keeps track of single "request" promise, so there is
// no overhead here.
done: () => Drafts.request().then(() => this.emit('fully-loaded'))
});
},
/**
* This method is responsible for adding or updating the data that could
* change in a node that's not moving in the same time.
*
* @param {Node} node The Node to update.
* @param {Object} record An object that holds the conversation informations.
*/
updateConversationContent(node, record) {
var id = record.id;
var thread = Threads.get(id);
var isDraft = thread.isDraft;
var draft = thread.getDraft();
var body = record.body;
var type = record.lastMessageType;
var iconLabel = '';
// If the draft is newer than the message, update
// the body with the draft content's first string.
if (draft && draft.timestamp >= record.timestamp) {
body = draft.content.find((content) => typeof content === 'string');
type = draft.type;
}
node.dataset.lastMessageType = type;
if (draft) {
// Set the "draft" visual indication
node.classList.add('draft');
if (isDraft) {
node.dataset.draftId = draft.id;
node.classList.add('is-draft');
iconLabel = 'is-draft';
} else {
node.classList.add('has-draft');
iconLabel = 'has-draft';
}
} else {
// remove it
node.classList.remove('draft', 'has-draft', 'is-draft');
}
if (record.unreadCount > 0) {
node.classList.add('unread');
iconLabel = 'unread-thread';
} else {
node.classList.remove('unread');
}
node.querySelector('.js-conversation-body').textContent = body || '';
var stateIndicator = node.querySelector('.js-conversation-state');
if (iconLabel) {
document.l10n.setAttributes(stateIndicator, iconLabel);
} else {
stateIndicator.removeAttribute('data-l10n-id');
}
},
createThread: function inbox_createThread(record) {
// Create DOM element
var li = document.createElement('li');
var timestamp = +record.timestamp;
var id = record.id;
var thread = Threads.get(id);
// A "new message" draft
var isDraft = thread.isDraft;
// An existing conversation's draft.
var draft = thread.getDraft();
li.id = 'thread-' + id;
li.dataset.threadId = id;
li.dataset.time = timestamp;
li.classList.add('threadlist-item');
// Render markup with thread data
li.innerHTML = this.tmpl.thread.interpolate({
hash: isDraft ? '#/composer' : '#/thread?id=' + id,
mode: isDraft ? 'drafts' : 'threads',
id: String(isDraft ? draft.id : id),
timestamp: String(timestamp)
}, {
safe: ['id']
});
this.updateConversationContent(li, record);
TimeHeaders.update(li.querySelector('time'));
return li;
},
deleteThread: function(threadId) {
// Threads.delete will handle deleting
// any Draft objects associated with the
// specified threadId.
Threads.delete(threadId);
// Cleanup the DOM
this.removeConversationDOM(threadId);
// Remove notification if exist
Utils.closeNotificationsForThread(threadId);
},
insertThreadContainer:
function inbox_insertThreadContainer(group, timestamp) {
// We look for placing the group in the right place.
var headers = InboxView.container.getElementsByTagName('header');
var groupFound = false;
for (var i = 0; i < headers.length; i++) {
if (timestamp >= headers[i].dataset.time) {
groupFound = true;
InboxView.container.insertBefore(group, headers[i].parentNode);
break;
}
}
if (!groupFound) {
InboxView.container.appendChild(group);
}
},
/**
* Update the DOM element for a conversation.
*
* @param {Record} record This contains the conversation informations.
* @param {Object} [options] Various options or hints to make the update more
* efficient.
* @param {Boolean} [options.conversationDraft] True if this update comes from
* a draft change for this thread. Will be false for "isDraft" conversation
* nodes.
* @param {Boolean} [options.deleted] True if this update comes from a message
* deletion.
* @param {Boolean} [options.unread] True if we have a new unread message in
* this conversation.
*/
updateThread(record, options = {}) {
var thread = Thread.create(record, options);
var threadUINode = document.getElementById('thread-' + thread.id);
var threadUITime = threadUINode ? +threadUINode.dataset.time : NaN;
var recordTime = +thread.timestamp;
Threads.set(thread.id, thread);
// Edge case: if we just received a message that is older than the latest
// one in the conversation, we only need to update the 'unread' status.
var newMessageReceived = options.unread;
if (newMessageReceived && threadUITime > recordTime) {
this.mark(thread.id, 'unread');
return;
}
// If we just deleted messages in a conversation but kept the last message
// unchanged, we don't need to update the conversation UI.
if (options.deleted && threadUITime === recordTime) {
return;
}
if (threadUINode && options.conversationDraft) {
// hasDraft drafts do not change the timestamps, so we can
// change the body and the type only.
this.updateConversationContent(threadUINode, record);
return;
}
// General case: update the conversation UI.
if (threadUINode) {
// remove the current node in order to place the new one properly.
this.removeConversationDOM(thread.id);
}
this.setEmpty(false);
if (this.appendThread(thread)) {
this.sticky && this.sticky.refresh();
}
},
onMessageSending: function inbox_onMessageSending(e) {
this.updateThread(e.message);
},
onMessageReceived: function inbox_onMessageReceived(e) {
// If user currently in the same thread, then mark thread as read
var markAsRead = Navigation.isCurrentPanel('thread', {
id: e.message.threadId
});
this.updateThread(e.message, { unread: !markAsRead });
},
onThreadsDeleted: function inbox_onThreadDeleted(e) {
e.ids.forEach(function(threadId) {
if (Threads.has(threadId)) {
this.deleteThread(threadId);
}
}, this);
},
/**
* Append a thread to the global threads container. Creates a time container
* (i.e. for a day or some other time period) for this thread if it doesn't
* exist already.
*
* @return Boolean true if a time container was created, false otherwise
*/
appendThread: function inbox_appendThread(thread) {
var timestamp = +thread.timestamp;
var firstThreadInContainer = false;
// We create the DOM element of the thread
var node = this.createThread(thread);
// Update info given a number
this.setContact(node);
// Is there any container already?
var threadsContainerID = 'threadsContainer_' +
Utils.getDayDate(timestamp);
var threadsContainer = document.getElementById(threadsContainerID);
// If there is no container we create & insert it to the DOM
if (!threadsContainer) {
// We create the wrapper with a 'header' & 'ul'
var threadsContainerWrapper =
InboxView.createThreadContainer(timestamp);
// Update threadsContainer with the new value
threadsContainer = threadsContainerWrapper.childNodes[1];
// Place our new content in the DOM
InboxView.insertThreadContainer(threadsContainerWrapper, timestamp);
// We had to create a container, so this will be the first thread in it.
firstThreadInContainer = true;
}
// Where have I to place the new thread?
var threads = threadsContainer.getElementsByTagName('li');
var threadFound = false;
for (var i = 0, l = threads.length; i < l; i++) {
if (timestamp > threads[i].dataset.time) {
threadFound = true;
threadsContainer.insertBefore(node, threads[i]);
break;
}
}
if (!threadFound) {
threadsContainer.appendChild(node);
}
if (this.inEditMode) {
// Remove the new added thread id from the selection handler
this.selectionHandler.unselect(thread.id);
this.updateSelectionStatus();
}
return firstThreadInContainer;
},
// Adds a new grouping header if necessary (today, tomorrow, ...)
createThreadContainer: function inbox_createThreadContainer(timestamp) {
var threadContainer = document.createElement('div');
// Create Header DOM Element
var headerDOM = document.createElement('header');
// The id is used by the sticky header code as the -moz-element target.
headerDOM.id = 'header_' + timestamp;
// Append 'time-update' state
headerDOM.dataset.timeUpdate = 'repeat';
headerDOM.dataset.time = timestamp;
headerDOM.dataset.dateOnly = true;
// Create UL DOM Element
var threadsContainerDOM = document.createElement('ul');
threadsContainerDOM.id = 'threadsContainer_' +
Utils.getDayDate(timestamp);
// Add text
Utils.setHeaderDate({
time: timestamp,
element: headerDOM
});
// Add to DOM all elements
threadContainer.appendChild(headerDOM);
threadContainer.appendChild(threadsContainerDOM);
return threadContainer;
},
// Method for updating all contact info after creating a contact
updateContactsInfo: function inbox_updateContactsInfo() {
Contacts.clearUnknown();
// Prevents cases where updateContactsInfo method is called
// before InboxView.container exists (as observed by errors
// in the js console)
if (!this.container) {
return;
}
// Retrieve all 'li' elements
var threads = this.container.getElementsByTagName('li');
[].forEach.call(threads, this.setContact.bind(this));
},
mark: function inbox_mark(id, current) {
var li = document.getElementById('thread-' + id);
if (li) {
li.classList.toggle('unread', current === 'unread');
}
},
onDraftDeleted: function inbox_onDraftDeleted(draft) {
var thread = Threads.get(draft.threadId || draft.id);
if (!thread) {
return;
}
if (thread.isDraft) {
this.deleteThread(thread.id);
} else {
this.updateThread(thread, { conversationDraft: true });
}
// If the draft we scheduled notification for has been deleted, we shouldn't
// notify user anymore.
if (draft.id === this.notifyAboutSavedDraftWithId) {
this.notifyAboutSavedDraftWithId = null;
}
},
onDraftSaved: function inbox_onDraftSaved(draft) {
if (draft.threadId) {
this.updateThread(
Threads.get(draft.threadId), { conversationDraft: true }
);
} else {
this.updateThread(draft);
}
// In case user saved draft when Inbox was not the active view, we want to
// notify that save operation successfully completed once user returns back
// to Inbox view.
if (!Navigation.isCurrentPanel('thread-list')) {
this.notifyAboutSavedDraftWithId = draft.id;
}
},
showDraftSavedBanner: function() {
this.draftSavedBanner.classList.remove('hide');
clearTimeout(this.timeouts.onDraftSaved);
this.timeouts.onDraftSaved = null;
this.timeouts.onDraftSaved = setTimeout(
() => this.draftSavedBanner.classList.add('hide'),
this.DRAFT_SAVED_DURATION
);
}
};
exports.InboxView = EventDispatcher.mixin(
InboxView, ['visually-loaded', 'fully-loaded']
);
}(this));