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