8fa739828bf822a763b7b4d75208438247f9c302
[moodle.git] / message / amd / src / message_drawer_view_overview_section.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Controls a section of the overview page in the message drawer.
18  *
19  * @module     core_message/message_drawer_view_overview_section
20  * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
23 define(
24 [
25     'jquery',
26     'core/custom_interaction_events',
27     'core/notification',
28     'core/pubsub',
29     'core/str',
30     'core/pending',
31     'core/templates',
32     'core/user_date',
33     'core_message/message_repository',
34     'core_message/message_drawer_events',
35     'core_message/message_drawer_router',
36     'core_message/message_drawer_routes',
37     'core_message/message_drawer_lazy_load_list',
38     'core_message/message_drawer_view_conversation_constants'
39 ],
40 function(
41     $,
42     CustomEvents,
43     Notification,
44     PubSub,
45     Str,
46     Pending,
47     Templates,
48     UserDate,
49     MessageRepository,
50     MessageDrawerEvents,
51     MessageDrawerRouter,
52     MessageDrawerRoutes,
53     LazyLoadList,
54     MessageDrawerViewConversationContants
55 ) {
57     var SELECTORS = {
58         TOGGLE: '[data-region="toggle"]',
59         CONVERSATION: '[data-conversation-id]',
60         BLOCKED_ICON_CONTAINER: '[data-region="contact-icon-blocked"]',
61         LAST_MESSAGE: '[data-region="last-message"]',
62         LAST_MESSAGE_DATE: '[data-region="last-message-date"]',
63         MUTED_ICON_CONTAINER: '[data-region="muted-icon-container"]',
64         UNREAD_COUNT: '[data-region="unread-count"]',
65         SECTION_TOTAL_COUNT: '[data-region="section-total-count"]',
66         SECTION_TOTAL_COUNT_CONTAINER: '[data-region="section-total-count-container"]',
67         SECTION_UNREAD_COUNT: '[data-region="section-unread-count"]',
68         PLACEHOLDER_CONTAINER: '[data-region="placeholder-container"]'
69     };
71     var TEMPLATES = {
72         CONVERSATIONS_LIST: 'core_message/message_drawer_conversations_list',
73         CONVERSATIONS_LIST_ITEMS_PLACEHOLDER: 'core_message/message_drawer_conversations_list_items_placeholder'
74     };
76     var LOAD_LIMIT = 50;
77     var loadedConversationsById = {};
78     var loadedTotalCounts = false;
79     var loadedUnreadCounts = false;
81     /**
82      * Get the section visibility status.
83      *
84      * @param  {Object} root The section container element.
85      * @return {Bool} Is section visible.
86      */
87     var isVisible = function(root) {
88         return LazyLoadList.getRoot(root).hasClass('show');
89     };
91     /**
92      * Set this section as expanded.
93      *
94      * @param  {Object} root The section container element.
95      */
96     var setExpanded = function(root) {
97         root.addClass('expanded');
98     };
100     /**
101      * Set this section as collapsed.
102      *
103      * @param  {Object} root The section container element.
104      */
105     var setCollapsed = function(root) {
106         root.removeClass('expanded');
107     };
109     /**
110      * Render the total count value and show it for the user. Also update the placeholder
111      * HTML for better visuals.
112      *
113      * @param {Object} root The section container element.
114      * @param {Number} count The total count
115      */
116     var renderTotalCount = function(root, count) {
117         var container = root.find(SELECTORS.SECTION_TOTAL_COUNT_CONTAINER);
118         var countElement = container.find(SELECTORS.SECTION_TOTAL_COUNT);
119         countElement.text(count);
120         container.removeClass('hidden');
121         Str.get_string('totalconversations', 'core_message', count).done(function(string) {
122             container.attr('aria-label', string);
123         });
125         var numPlaceholders = count > 20 ? 20 : count;
126         // Array of "true" up to the number of placeholders we want.
127         var placeholders = Array.apply(null, Array(numPlaceholders)).map(function() {
128             return true;
129         });
131         // Replace the current placeholder (loading spinner) with some nicer placeholders that
132         // better represent the content.
133         Templates.render(TEMPLATES.CONVERSATIONS_LIST_ITEMS_PLACEHOLDER, {placeholders: placeholders})
134             .then(function(html) {
135                 var placeholderContainer = root.find(SELECTORS.PLACEHOLDER_CONTAINER);
136                 placeholderContainer.html(html);
137                 return;
138             })
139             .catch(function() {
140                 // Silently ignore. Doesn't matter if we can't render the placeholders.
141             });
142     };
144     /**
145      * Render the unread count value and show it for the user if it's higher than zero.
146      *
147      * @param {Object} root The section container element.
148      * @param {Number} count The unread count
149      */
150     var renderUnreadCount = function(root, count) {
151         var countElement = root.find(SELECTORS.SECTION_UNREAD_COUNT);
152         countElement.text(count);
154         Str.get_string('unreadconversations', 'core_message', count).done(function(string) {
155             countElement.attr('aria-label', string);
156         });
158         if (count > 0) {
159             countElement.removeClass('hidden');
160         }
161     };
163     /**
164      * Create a formatted conversation object from the the one we get from events. The new object
165      * will be in a format that matches what we receive from the server.
166      *
167      * @param {Object} conversation
168      * @return {Object} formatted conversation.
169      */
170     var formatConversationFromEvent = function(conversation) {
171         // Recursively lowercase all of the keys for an object.
172         var recursivelyLowercaseKeys = function(object) {
173             return Object.keys(object).reduce(function(carry, key) {
174                 if ($.isArray(object[key])) {
175                     carry[key.toLowerCase()] = object[key].map(recursivelyLowercaseKeys);
176                 } else {
177                     carry[key.toLowerCase()] = object[key];
178                 }
180                 return carry;
181             }, {});
182         };
184         // Recursively lowercase all of the keys for the conversation.
185         var formatted = recursivelyLowercaseKeys(conversation);
187         // Make sure all messages have the useridfrom property set.
188         formatted.messages = formatted.messages.map(function(message) {
189             message.useridfrom = message.userfrom.id;
190             return message;
191         });
193         return formatted;
194     };
196     /**
197      * Render the messages in the overview page.
198      *
199      * @param {Object} contentContainer Conversations content container.
200      * @param {Array} conversations List of conversations to render.
201      * @param {Number} userId Logged in user id.
202      * @return {Object} jQuery promise.
203      */
204     var render = function(conversations, userId) {
206         // Helper to format the last message for rendering.
207         // Returns a promise which resolves to either a string, or null
208         // (such as in the event of an empty personal space).
209         var pending = new Pending();
211         var formatMessagePreview = async function(lastMessage) {
212             if (!lastMessage) {
213                 return null;
214             }
215             // Check the message html for a src attribute, indicative of media.
216             // Replace <img with <noimg to stop browsers pre-fetching the image as part of tmp element creation.
217             var tmpElement = document.createElement("element");
218             tmpElement.innerHTML = lastMessage.text.replace(/<img /g, '<noimg ');
219             var isMedia = tmpElement.querySelector("[src]") || false;
221             if (!isMedia) {
222                 // Try to get the text value of the content.
223                 // If that's not possible, we'll report it under the catch-all 'other media'.
224                 var messagePreview = $(lastMessage.text).text();
225                 if (messagePreview) {
226                     return messagePreview;
227                 }
228             }
230             // As a fallback, report unknowns as 'other media' type content.
231             var pix = 'i/messagecontentmultimediageneral';
232             var label = 'messagecontentmultimediageneral';
234             if (lastMessage.text.includes('<img')) {
235                 pix = 'i/messagecontentimage';
236                 label = 'messagecontentimage';
237             } else if (lastMessage.text.includes('<video')) {
238                 pix = 'i/messagecontentvideo';
239                 label = 'messagecontentvideo';
240             } else if (lastMessage.text.includes('<audio')) {
241                 pix = 'i/messagecontentaudio';
242                 label = 'messagecontentaudio';
243             }
245             try {
246                 var labelString = await Str.get_string(label, 'core_message');
247                 var icon = await Templates.renderPix(pix, 'core', labelString);
248                 return icon + ' ' + labelString;
249             } catch (error) {
250                 Notification.exception(error);
251                 return null;
252             }
253         };
255         var mapPromises = conversations.map(function(conversation) {
257             var lastMessage = conversation.messages.length ? conversation.messages[conversation.messages.length - 1] : null;
259             return formatMessagePreview(lastMessage)
260                 .then(function(messagePreview) {
261                     var formattedConversation = {
262                         id: conversation.id,
263                         imageurl: conversation.imageurl,
264                         name: conversation.name,
265                         subname: conversation.subname,
266                         unreadcount: conversation.unreadcount,
267                         ismuted: conversation.ismuted,
268                         lastmessagedate: lastMessage ? lastMessage.timecreated : null,
269                         sentfromcurrentuser: lastMessage ? lastMessage.useridfrom == userId : null,
270                         lastmessage: messagePreview
271                     };
273                     var otherUser = null;
274                     if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF) {
275                         // Self-conversations have only one member.
276                         otherUser = conversation.members[0];
277                     } else if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
278                         // For private conversations, remove the current userId from the members to get the other user.
279                         otherUser = conversation.members.reduce(function(carry, member) {
280                             if (!carry && member.id != userId) {
281                                 carry = member;
282                             }
283                             return carry;
284                         }, null);
285                     }
287                     if (otherUser !== null) {
288                         formattedConversation.userid = otherUser.id;
289                         formattedConversation.showonlinestatus = otherUser.showonlinestatus;
290                         formattedConversation.isonline = otherUser.isonline;
291                         formattedConversation.isblocked = otherUser.isblocked;
292                     }
294                     if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PUBLIC) {
295                         formattedConversation.lastsendername = conversation.members.reduce(function(carry, member) {
296                             if (!carry && lastMessage && member.id == lastMessage.useridfrom) {
297                                 carry = member.fullname;
298                             }
299                             return carry;
300                         }, null);
301                     }
303                     return formattedConversation;
304                 }).catch(Notification.exception);
305         });
307         return Promise.all(mapPromises)
308             .then(function(formattedConversations) {
309                 formattedConversations.forEach(function(conversation) {
310                     if (new Date().toDateString() == new Date(conversation.lastmessagedate * 1000).toDateString()) {
311                         conversation.istoday = true;
312                     }
313                 });
315                 return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: formattedConversations});
316             }).then(function(html, js) {
317                 pending.resolve();
318                 return $.Deferred().resolve(html, js);
319             }).catch(function(error) {
320                 pending.resolve();
321                 Notification.exception(error);
322             });
323     };
325     /**
326      * Build the callback to load conversations.
327      *
328      * @param  {Array|null} types The conversation types for this section.
329      * @param  {bool} includeFavourites Include/exclude favourites.
330      * @param  {Number} offset Result offset
331      * @return {Function}
332      */
333     var getLoadCallback = function(types, includeFavourites, offset) {
334         // Note: This function is a bit messy because we've added the concept of loading
335         // multiple conversations types (e.g. private + self) at once but haven't properly
336         // updated the web service to accept an array of types. Instead we've added a new
337         // parameter for the self type which means we can only ever load self + other type.
338         // This should be improved to make it more extensible in the future. Adding new params
339         // for each type isn't very scalable.
340         var type = null;
341         // Include self conversations in the results by default.
342         var includeSelfConversations = true;
343         if (types && types.length) {
344             // Just get the conversation types that aren't "self" for now.
345             var nonSelfConversationTypes = types.filter(function(candidate) {
346                 return candidate != MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF;
347             });
348             // If we're specifically asking for a list of types that doesn't include the self
349             // conversations then we don't need to include them.
350             includeSelfConversations = types.length != nonSelfConversationTypes.length;
351             // As mentioned above the webservice is currently limited to loading one type at a
352             // time (plus self conversations) so let's hope we never change this.
353             type = nonSelfConversationTypes[0];
354         }
356         return function(root, userId) {
357             return MessageRepository.getConversations(
358                     userId,
359                     type,
360                     LOAD_LIMIT + 1,
361                     offset,
362                     includeFavourites,
363                     includeSelfConversations
364                 )
365                 .then(function(response) {
366                     var conversations = response.conversations;
368                     if (conversations.length > LOAD_LIMIT) {
369                         conversations = conversations.slice(0, -1);
370                     } else {
371                         LazyLoadList.setLoadedAll(root, true);
372                     }
374                     offset = offset + LOAD_LIMIT;
376                     conversations.forEach(function(conversation) {
377                         loadedConversationsById[conversation.id] = conversation;
378                     });
380                     return conversations;
381                 })
382                 .catch(Notification.exception);
383         };
384     };
386     /**
387      * Get the total count container element.
388      *
389      * @param  {Object} root Overview messages container element.
390      * @return {Object} Total count container element.
391      */
392     var getTotalConversationCountElement = function(root) {
393         return root.find(SELECTORS.SECTION_TOTAL_COUNT);
394     };
396     /**
397      * Get the unread conversations count container element.
398      *
399      * @param  {Object} root Overview messages container element.
400      * @return {Object} Unread conversations count container element.
401      */
402     var getTotalUnreadConversationCountElement = function(root) {
403         return root.find(SELECTORS.SECTION_UNREAD_COUNT);
404     };
406     /**
407      * Increment the total conversations count.
408      *
409      * @param  {Object} root Overview messages container element.
410      */
411     var incrementTotalConversationCount = function(root) {
412         if (loadedTotalCounts) {
413             var element = getTotalConversationCountElement(root);
414             var count = parseInt(element.text());
415             count = count + 1;
416             element.text(count);
417         }
418     };
420     /**
421      * Decrement the total conversations count.
422      *
423      * @param  {Object} root Overview messages container element.
424      */
425     var decrementTotalConversationCount = function(root) {
426         if (loadedTotalCounts) {
427             var element = getTotalConversationCountElement(root);
428             var count = parseInt(element.text());
429             count = count - 1;
430             element.text(count);
431         }
432     };
434     /**
435      * Decrement the total unread conversations count.
436      *
437      * @param  {Object} root Overview messages container element.
438      */
439     var decrementTotalUnreadConversationCount = function(root) {
440         if (loadedUnreadCounts) {
441             var element = getTotalUnreadConversationCountElement(root);
442             var count = parseInt(element.text());
443             count = count - 1;
444             element.text(count);
446             if (count < 1) {
447                 element.addClass('hidden');
448             }
449         }
450     };
452     /**
453      * Get a contact / conversation element.
454      *
455      * @param  {Object} root Overview messages container element.
456      * @param  {Number} conversationId The conversation id.
457      * @return {Object} Conversation element.
458      */
459     var getConversationElement = function(root, conversationId) {
460         return root.find('[data-conversation-id="' + conversationId + '"]');
461     };
463     /**
464      * Get a contact / conversation element from a user id.
465      *
466      * @param  {Object} root Overview messages container element.
467      * @param  {Number} userId The user id.
468      * @return {Object} Conversation element.
469      */
470     var getConversationElementFromUserId = function(root, userId) {
471         return root.find('[data-user-id="' + userId + '"]');
472     };
474     /**
475      * Show the conversation is muted icon.
476      *
477      * @param  {Object} conversationElement The conversation element.
478      */
479     var muteConversation = function(conversationElement) {
480         conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).removeClass('hidden');
481     };
483     /**
484      * Hide the conversation is muted icon.
485      *
486      * @param  {Object} conversationElement The conversation element.
487      */
488     var unmuteConversation = function(conversationElement) {
489         conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).addClass('hidden');
490     };
492     /**
493      * Show the contact is blocked icon.
494      *
495      * @param  {Object} conversationElement The conversation element.
496      */
497     var blockContact = function(conversationElement) {
498         conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).removeClass('hidden');
499     };
501     /**
502      * Hide the contact is blocked icon.
503      *
504      * @param  {Object} conversationElement The conversation element.
505      */
506     var unblockContact = function(conversationElement) {
507         conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).addClass('hidden');
508     };
510     /**
511      * Create an render new conversation element in the list of conversations.
512      *
513      * @param  {Object} root Overview messages container element.
514      * @param  {Object} conversation The conversation.
515      * @param  {Number} userId The logged in user id.
516      * @return {Object} jQuery promise
517      */
518     var createNewConversationFromEvent = function(root, conversation, userId) {
519         var existingConversations = root.find(SELECTORS.CONVERSATION);
521         if (!existingConversations.length) {
522             // If we didn't have any conversations then we need to show
523             // the content of the list and hide the empty message.
524             var listRoot = LazyLoadList.getRoot(root);
525             LazyLoadList.showContent(listRoot);
526             LazyLoadList.hideEmptyMessage(listRoot);
527         }
529         // Cache the conversation.
530         loadedConversationsById[conversation.id] = conversation;
532         return render([conversation], userId)
533             .then(function(html) {
534                 var contentContainer = LazyLoadList.getContentContainer(root);
535                 return contentContainer.prepend(html);
536             })
537             .then(function() {
538                 return incrementTotalConversationCount(root);
539             })
540             .catch(Notification.exception);
541     };
543     /**
544      * Delete a conversation from the list of conversations.
545      *
546      * @param  {Object} root Overview messages container element.
547      * @param  {Object} conversationElement The conversation element.
548      */
549     var deleteConversation = function(root, conversationElement) {
550         conversationElement.remove();
551         decrementTotalConversationCount(root);
553         var conversations = root.find(SELECTORS.CONVERSATION);
554         if (!conversations.length) {
555             // If we don't have any conversations then we need to hide
556             // the content of the list and show the empty message.
557             var listRoot = LazyLoadList.getRoot(root);
558             LazyLoadList.hideContent(listRoot);
559             LazyLoadList.showEmptyMessage(listRoot);
560         }
561     };
563     /**
564      * Mark a conversation as read.
565      *
566      * @param  {Object} root Overview messages container element.
567      * @param  {Object} conversationElement The conversation element.
568      */
569     var markConversationAsRead = function(root, conversationElement) {
570         var unreadCount = conversationElement.find(SELECTORS.UNREAD_COUNT);
571         unreadCount.text('0');
572         unreadCount.addClass('hidden');
573         decrementTotalUnreadConversationCount(root);
574     };
576     /**
577      * Listen to, and handle events in this section.
578      *
579      * @param {String} namespace Unique identifier for the Routes
580      * @param {Object} root The section container element.
581      * @param {Function} loadCallback The callback to load items.
582      * @param {Array|null} types The conversation types for this section
583      * @param {bool} includeFavourites If this section includes favourites
584      * @param {String} fromPanel Routing argument to send if the section is loaded in message index left panel.
585      */
586     var registerEventListeners = function(namespace, root, loadCallback, types, includeFavourites, fromPanel) {
587         var listRoot = LazyLoadList.getRoot(root);
588         var conversationBelongsToThisSection = function(conversation) {
589             // Make sure the type is an int so that the index of check matches correctly.
590             var conversationType = parseInt(conversation.type, 10);
591             if (
592                 // If the conversation type isn't one this section cares about then we can ignore it.
593                 (types && types.indexOf(conversationType) < 0) ||
594                 // If this is the favourites section and the conversation isn't a favourite then ignore it.
595                 (includeFavourites && !conversation.isFavourite) ||
596                 // If this section doesn't include favourites and the conversation is a favourite then ignore it.
597                 (!includeFavourites && conversation.isFavourite)
598             ) {
599                 return false;
600             }
602             return true;
603         };
605         // Set the minimum height of the section to the height of the toggle. This
606         // smooths out the collapse animation.
607         var toggle = root.find(SELECTORS.TOGGLE);
608         root.css('min-height', toggle.outerHeight());
610         root.on('show.bs.collapse', function() {
611             setExpanded(root);
612             LazyLoadList.show(listRoot, loadCallback, function(contentContainer, conversations, userId) {
613                 return render(conversations, userId)
614                     .then(function(html) {
615                         contentContainer.append(html);
616                         return html;
617                     })
618                     .catch(Notification.exception);
619             });
620         });
622         root.on('hidden.bs.collapse', function() {
623             setCollapsed(root);
624         });
626         PubSub.subscribe(MessageDrawerEvents.CONTACT_BLOCKED, function(userId) {
627             var conversationElement = getConversationElementFromUserId(root, userId);
628             if (conversationElement.length) {
629                 blockContact(conversationElement);
630             }
631         });
633         PubSub.subscribe(MessageDrawerEvents.CONTACT_UNBLOCKED, function(userId) {
634             var conversationElement = getConversationElementFromUserId(root, userId);
636             if (conversationElement.length) {
637                 unblockContact(conversationElement);
638             }
639         });
641         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_MUTED, function(conversation) {
642             var conversationId = conversation.id;
643             var conversationElement = getConversationElement(root, conversationId);
644             if (conversationElement.length) {
645                 muteConversation(conversationElement);
646             }
647         });
649         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_MUTED, function(conversation) {
650             var conversationId = conversation.id;
651             var conversationElement = getConversationElement(root, conversationId);
652             if (conversationElement.length) {
653                 unmuteConversation(conversationElement);
654             }
655         });
657         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, function(conversation) {
658             if (!conversationBelongsToThisSection(conversation)) {
659                 return;
660             }
662             var loggedInUserId = conversation.loggedInUserId;
663             var conversationId = conversation.id;
664             var element = getConversationElement(root, conversationId);
665             conversation = formatConversationFromEvent(conversation);
666             if (element.length) {
667                 var contentContainer = LazyLoadList.getContentContainer(root);
668                 render([conversation], loggedInUserId)
669                     .then(function(html) {
670                             contentContainer.prepend(html);
671                             element.remove();
672                             return html;
673                         })
674                     .catch(Notification.exception);
675             } else {
676                 createNewConversationFromEvent(root, conversation, loggedInUserId);
677             }
678         });
680         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_DELETED, function(conversationId) {
681             var conversationElement = getConversationElement(root, conversationId);
682             delete loadedConversationsById[conversationId];
683             if (conversationElement.length) {
684                 deleteConversation(root, conversationElement);
685             }
686         });
688         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, function(conversationId) {
689             var conversationElement = getConversationElement(root, conversationId);
690             if (conversationElement.length) {
691                 markConversationAsRead(root, conversationElement);
692             }
693         });
695         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_FAVOURITE, function(conversation) {
696             var conversationElement = null;
697             if (conversationBelongsToThisSection(conversation)) {
698                 conversationElement = getConversationElement(root, conversation.id);
699                 if (!conversationElement.length) {
700                     createNewConversationFromEvent(
701                         root,
702                         formatConversationFromEvent(conversation),
703                         conversation.loggedInUserId
704                     );
705                 }
706             } else {
707                 conversationElement = getConversationElement(root, conversation.id);
708                 if (conversationElement.length) {
709                     deleteConversation(root, conversationElement);
710                 }
711             }
712         });
714         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE, function(conversation) {
715             var conversationElement = null;
716             if (conversationBelongsToThisSection(conversation)) {
717                 conversationElement = getConversationElement(root, conversation.id);
718                 if (!conversationElement.length) {
719                     createNewConversationFromEvent(
720                         root,
721                         formatConversationFromEvent(conversation),
722                         conversation.loggedInUserId
723                     );
724                 }
725             } else {
726                 conversationElement = getConversationElement(root, conversation.id);
727                 if (conversationElement.length) {
728                     deleteConversation(root, conversationElement);
729                 }
730             }
731         });
733         CustomEvents.define(root, [CustomEvents.events.activate]);
734         root.on(CustomEvents.events.activate, SELECTORS.CONVERSATION, function(e, data) {
735             var conversationElement = $(e.target).closest(SELECTORS.CONVERSATION);
736             var conversationId = conversationElement.attr('data-conversation-id');
737             var conversation = loadedConversationsById[conversationId];
738             MessageDrawerRouter.go(namespace, MessageDrawerRoutes.VIEW_CONVERSATION, conversation, fromPanel);
740             data.originalEvent.preventDefault();
741         });
742     };
744     /**
745      * Setup the section.
746      *
747      * @param {String} namespace Unique identifier for the Routes
748      * @param {Object} header The header container element.
749      * @param {Object} body The section container element.
750      * @param {Object} footer The footer container element.
751      * @param {Array} types The conversation types that show in this section
752      * @param {bool} includeFavourites If this section includes favourites
753      * @param {Object} totalCountPromise Resolves wth the total conversations count
754      * @param {Object} unreadCountPromise Resolves wth the unread conversations count
755      * @param {bool} fromPanel shown in message app panel.
756      */
757     var show = function(namespace, header, body, footer, types, includeFavourites, totalCountPromise, unreadCountPromise,
758         fromPanel) {
759         var root = $(body);
761         if (!root.attr('data-init')) {
762             var loadCallback = getLoadCallback(types, includeFavourites, 0);
763             registerEventListeners(namespace, root, loadCallback, types, includeFavourites, fromPanel);
765             if (isVisible(root)) {
766                 setExpanded(root);
767                 var listRoot = LazyLoadList.getRoot(root);
768                 LazyLoadList.show(listRoot, loadCallback, function(contentContainer, conversations, userId) {
769                     return render(conversations, userId)
770                         .then(function(html) {
771                             contentContainer.append(html);
772                             return html;
773                         })
774                         .catch(Notification.exception);
775                 });
776             }
778             // This is given to us by the calling code because the total counts for all sections
779             // are loaded in a single ajax request rather than one request per section.
780             totalCountPromise.then(function(count) {
781                 renderTotalCount(root, count);
782                 loadedTotalCounts = true;
783                 return;
784             })
785             .catch(function() {
786                 // Silently ignore if we can't updated the counts. No need to bother the user.
787             });
789             // This is given to us by the calling code because the unread counts for all sections
790             // are loaded in a single ajax request rather than one request per section.
791             unreadCountPromise.then(function(count) {
792                 renderUnreadCount(root, count);
793                 loadedUnreadCounts = true;
794                 return;
795             })
796             .catch(function() {
797                 // Silently ignore if we can't updated the counts. No need to bother the user.
798             });
800             root.attr('data-init', true);
801         }
802     };
804     return {
805         show: show,
806         isVisible: isVisible
807     };
808 });