MDL-64715 message: improve the front-end for the self-conversations
[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/templates',
31     'core/user_date',
32     'core_message/message_repository',
33     'core_message/message_drawer_events',
34     'core_message/message_drawer_router',
35     'core_message/message_drawer_routes',
36     'core_message/message_drawer_lazy_load_list',
37     'core_message/message_drawer_view_conversation_constants'
38 ],
39 function(
40     $,
41     CustomEvents,
42     Notification,
43     PubSub,
44     Str,
45     Templates,
46     UserDate,
47     MessageRepository,
48     MessageDrawerEvents,
49     MessageDrawerRouter,
50     MessageDrawerRoutes,
51     LazyLoadList,
52     MessageDrawerViewConversationContants
53 ) {
55     var SELECTORS = {
56         TOGGLE: '[data-region="toggle"]',
57         CONVERSATION: '[data-conversation-id]',
58         BLOCKED_ICON_CONTAINER: '[data-region="contact-icon-blocked"]',
59         LAST_MESSAGE: '[data-region="last-message"]',
60         LAST_MESSAGE_DATE: '[data-region="last-message-date"]',
61         MUTED_ICON_CONTAINER: '[data-region="muted-icon-container"]',
62         UNREAD_COUNT: '[data-region="unread-count"]',
63         SECTION_TOTAL_COUNT: '[data-region="section-total-count"]',
64         SECTION_TOTAL_COUNT_CONTAINER: '[data-region="section-total-count-container"]',
65         SECTION_UNREAD_COUNT: '[data-region="section-unread-count"]',
66         PLACEHOLDER_CONTAINER: '[data-region="placeholder-container"]'
67     };
69     var TEMPLATES = {
70         CONVERSATIONS_LIST: 'core_message/message_drawer_conversations_list',
71         CONVERSATIONS_LIST_ITEMS_PLACEHOLDER: 'core_message/message_drawer_conversations_list_items_placeholder'
72     };
74     var LOAD_LIMIT = 50;
75     var loadedConversationsById = {};
76     var loadedTotalCounts = false;
77     var loadedUnreadCounts = false;
79     /**
80      * Get the section visibility status.
81      *
82      * @param  {Object} root The section container element.
83      * @return {Bool} Is section visible.
84      */
85     var isVisible = function(root) {
86         return LazyLoadList.getRoot(root).hasClass('show');
87     };
89     /**
90      * Set this section as expanded.
91      *
92      * @param  {Object} root The section container element.
93      */
94     var setExpanded = function(root) {
95         root.addClass('expanded');
96     };
98     /**
99      * Set this section as collapsed.
100      *
101      * @param  {Object} root The section container element.
102      */
103     var setCollapsed = function(root) {
104         root.removeClass('expanded');
105     };
107     /**
108      * Render the total count value and show it for the user. Also update the placeholder
109      * HTML for better visuals.
110      *
111      * @param {Object} root The section container element.
112      * @param {Number} count The total count
113      */
114     var renderTotalCount = function(root, count) {
115         var container = root.find(SELECTORS.SECTION_TOTAL_COUNT_CONTAINER);
116         var countElement = container.find(SELECTORS.SECTION_TOTAL_COUNT);
117         countElement.text(count);
118         container.removeClass('hidden');
119         Str.get_string('totalconversations', 'core_message', count).done(function(string) {
120             container.attr('aria-label', string);
121         });
123         var numPlaceholders = count > 20 ? 20 : count;
124         // Array of "true" up to the number of placeholders we want.
125         var placeholders = Array.apply(null, Array(numPlaceholders)).map(function() {
126             return true;
127         });
129         // Replace the current placeholder (loading spinner) with some nicer placeholders that
130         // better represent the content.
131         Templates.render(TEMPLATES.CONVERSATIONS_LIST_ITEMS_PLACEHOLDER, {placeholders: placeholders})
132             .then(function(html) {
133                 var placeholderContainer = root.find(SELECTORS.PLACEHOLDER_CONTAINER);
134                 placeholderContainer.html(html);
135                 return;
136             })
137             .catch(function() {
138                 // Silently ignore. Doesn't matter if we can't render the placeholders.
139             });
140     };
142     /**
143      * Render the unread count value and show it for the user if it's higher than zero.
144      *
145      * @param {Object} root The section container element.
146      * @param {Number} count The unread count
147      */
148     var renderUnreadCount = function(root, count) {
149         var countElement = root.find(SELECTORS.SECTION_UNREAD_COUNT);
150         countElement.text(count);
152         Str.get_string('unreadconversations', 'core_message', count).done(function(string) {
153             countElement.attr('aria-label', string);
154         });
156         if (count > 0) {
157             countElement.removeClass('hidden');
158         }
159     };
161     /**
162      * Render the messages in the overview page.
163      *
164      * @param {Object} contentContainer Conversations content container.
165      * @param {Array} conversations List of conversations to render.
166      * @param {Number} userId Logged in user id.
167      * @return {Object} jQuery promise.
168      */
169     var render = function(contentContainer, conversations, userId) {
170         var formattedConversations = conversations.map(function(conversation) {
172             var lastMessage = conversation.messages.length ? conversation.messages[conversation.messages.length - 1] : null;
174             var formattedConversation = {
175                 id: conversation.id,
176                 imageurl: conversation.imageurl,
177                 name: conversation.name,
178                 subname: conversation.subname,
179                 unreadcount: conversation.unreadcount,
180                 ismuted: conversation.ismuted,
181                 lastmessagedate: lastMessage ? lastMessage.timecreated : null,
182                 sentfromcurrentuser: lastMessage ? lastMessage.useridfrom == userId : null,
183                 lastmessage: lastMessage ? $(lastMessage.text).text() || lastMessage.text : null
184             };
186             var otherUser = null;
187             if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF) {
188                 // Self-conversations have only one member.
189                 otherUser = conversation.members[0];
190             } else if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
191                 // For private conversations, remove the current userId from the members to get the other user.
192                 otherUser = conversation.members.reduce(function(carry, member) {
193                     if (!carry && member.id != userId) {
194                         carry = member;
195                     }
196                     return carry;
197                 }, null);
198             }
200             if (otherUser !== null) {
201                 formattedConversation.userid = otherUser.id;
202                 formattedConversation.showonlinestatus = otherUser.showonlinestatus;
203                 formattedConversation.isonline = otherUser.isonline;
204                 formattedConversation.isblocked = otherUser.isblocked;
205             }
207             if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PUBLIC) {
208                 formattedConversation.lastsendername = conversation.members.reduce(function(carry, member) {
209                     if (!carry && member.id == lastMessage.useridfrom) {
210                         carry = member.fullname;
211                     }
212                     return carry;
213                 }, null);
214             }
216             return formattedConversation;
217         });
219         formattedConversations.forEach(function(conversation) {
220             if (new Date().toDateString() == new Date(conversation.lastmessagedate * 1000).toDateString()) {
221                 conversation.istoday = true;
222             }
223         });
225         return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: formattedConversations})
226             .then(function(html) {
227                 contentContainer.append(html);
228                 return html;
229             })
230             .catch(Notification.exception);
231     };
233     /**
234      * Build the callback to load conversations.
235      *
236      * @param  {Array|null} types The conversation types for this section.
237      * @param  {bool} includeFavourites Include/exclude favourites.
238      * @param  {Number} offset Result offset
239      * @return {Function}
240      */
241     var getLoadCallback = function(types, includeFavourites, offset) {
242         // Note: This function is a bit messy because we've added the concept of loading
243         // multiple conversations types (e.g. private + self) at once but haven't properly
244         // updated the web service to accept an array of types. Instead we've added a new
245         // parameter for the self type which means we can only ever load self + other type.
246         // This should be improved to make it more extensible in the future. Adding new params
247         // for each type isn't very scalable.
248         var type = null;
249         // Include self conversations in the results by default.
250         var includeSelfConversations = true;
251         if (types && types.length) {
252             // Just get the conversation types that aren't "self" for now.
253             var nonSelfConversationTypes = types.filter(function(candidate) {
254                 return candidate != MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF;
255             });
256             // If we're specifically asking for a list of types that doesn't include the self
257             // conversations then we don't need to include them.
258             includeSelfConversations = types.length != nonSelfConversationTypes.length;
259             // As mentioned above the webservice is currently limited to loading one type at a
260             // time (plus self conversations) so let's hope we never change this.
261             type = nonSelfConversationTypes[0];
262         }
264         return function(root, userId) {
265             return MessageRepository.getConversations(
266                     userId,
267                     type,
268                     LOAD_LIMIT + 1,
269                     offset,
270                     includeFavourites,
271                     includeSelfConversations
272                 )
273                 .then(function(response) {
274                     var conversations = response.conversations;
276                     if (conversations.length > LOAD_LIMIT) {
277                         conversations = conversations.slice(0, -1);
278                     } else {
279                         LazyLoadList.setLoadedAll(root, true);
280                     }
282                     offset = offset + LOAD_LIMIT;
284                     conversations.forEach(function(conversation) {
285                         loadedConversationsById[conversation.id] = conversation;
286                     });
288                     return conversations;
289                 })
290                 .catch(Notification.exception);
291         };
292     };
294     /**
295      * Get the total count container element.
296      *
297      * @param  {Object} root Overview messages container element.
298      * @return {Object} Total count container element.
299      */
300     var getTotalConversationCountElement = function(root) {
301         return root.find(SELECTORS.SECTION_TOTAL_COUNT);
302     };
304     /**
305      * Get the unread conversations count container element.
306      *
307      * @param  {Object} root Overview messages container element.
308      * @return {Object} Unread conversations count container element.
309      */
310     var getTotalUnreadConversationCountElement = function(root) {
311         return root.find(SELECTORS.SECTION_UNREAD_COUNT);
312     };
314     /**
315      * Increment the total conversations count.
316      *
317      * @param  {Object} root Overview messages container element.
318      */
319     var incrementTotalConversationCount = function(root) {
320         if (loadedTotalCounts) {
321             var element = getTotalConversationCountElement(root);
322             var count = parseInt(element.text());
323             count = count + 1;
324             element.text(count);
325         }
326     };
328     /**
329      * Decrement the total conversations count.
330      *
331      * @param  {Object} root Overview messages container element.
332      */
333     var decrementTotalConversationCount = function(root) {
334         if (loadedTotalCounts) {
335             var element = getTotalConversationCountElement(root);
336             var count = parseInt(element.text());
337             count = count - 1;
338             element.text(count);
339         }
340     };
342     /**
343      * Decrement the total unread conversations count.
344      *
345      * @param  {Object} root Overview messages container element.
346      */
347     var decrementTotalUnreadConversationCount = function(root) {
348         if (loadedUnreadCounts) {
349             var element = getTotalUnreadConversationCountElement(root);
350             var count = parseInt(element.text());
351             count = count - 1;
352             element.text(count);
354             if (count < 1) {
355                 element.addClass('hidden');
356             }
357         }
358     };
360     /**
361      * Get a contact / conversation element.
362      *
363      * @param  {Object} root Overview messages container element.
364      * @param  {Number} conversationId The conversation id.
365      * @return {Object} Conversation element.
366      */
367     var getConversationElement = function(root, conversationId) {
368         return root.find('[data-conversation-id="' + conversationId + '"]');
369     };
371     /**
372      * Get a contact / conversation element from a user id.
373      *
374      * @param  {Object} root Overview messages container element.
375      * @param  {Number} userId The user id.
376      * @return {Object} Conversation element.
377      */
378     var getConversationElementFromUserId = function(root, userId) {
379         return root.find('[data-user-id="' + userId + '"]');
380     };
382     /**
383      * Show the conversation is muted icon.
384      *
385      * @param  {Object} conversationElement The conversation element.
386      */
387     var muteConversation = function(conversationElement) {
388         conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).removeClass('hidden');
389     };
391     /**
392      * Hide the conversation is muted icon.
393      *
394      * @param  {Object} conversationElement The conversation element.
395      */
396     var unmuteConversation = function(conversationElement) {
397         conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).addClass('hidden');
398     };
400     /**
401      * Show the contact is blocked icon.
402      *
403      * @param  {Object} conversationElement The conversation element.
404      */
405     var blockContact = function(conversationElement) {
406         conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).removeClass('hidden');
407     };
409     /**
410      * Hide the contact is blocked icon.
411      *
412      * @param  {Object} conversationElement The conversation element.
413      */
414     var unblockContact = function(conversationElement) {
415         conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).addClass('hidden');
416     };
418     /**
419      * Update the last message from / to a contact.
420      *
421      * @param  {Object} element Conversation element.
422      * @param  {Object} conversation The conversation.
423      * @return {Object} jQuery promise
424      */
425     var updateLastMessage = function(element, conversation) {
426         var message = conversation.messages[conversation.messages.length - 1];
427         var senderString = '';
428         var senderStringRequest;
429         if (message.fromLoggedInUser) {
430             senderStringRequest = {key: 'you', component: 'core_message'};
431         } else {
432             senderStringRequest = {key: 'sender', component: 'core_message', param: message.userFrom.fullname};
433         }
435         var stringRequests = [
436             senderStringRequest,
437             {key: 'strftimetime24', component: 'core_langconfig'},
438         ];
439         return Str.get_strings(stringRequests)
440             .then(function(strings) {
441                 senderString = strings[0];
442                 return UserDate.get([{timestamp: message.timeCreated, format: strings[1]}]);
443             })
444             .then(function(dates) {
445                 return dates[0];
446             })
447             .then(function(dateString) {
448                 element.find(SELECTORS.LAST_MESSAGE_DATE).text(dateString).removeClass('hidden');
450                 // No need to show sender string for private conversations and where the last message didn't come from you.
451                 if (!message.fromLoggedInUser &&
452                         conversation.type === MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
453                     senderString = '';
454                 }
456                 // Now load the last message.
457                 var lastMessage = senderString + " <span class='text-muted'>" + $(message.text).text() + "</span>";
459                 return element.find(SELECTORS.LAST_MESSAGE).html(lastMessage);
460             });
461     };
463     /**
464      * Create an render new conversation element in the list of conversations.
465      *
466      * @param  {Object} root Overview messages container element.
467      * @param  {Object} conversation The conversation.
468      * @return {Object} jQuery promise
469      */
470     var createNewConversation = function(root, conversation) {
471         var existingConversations = root.find(SELECTORS.CONVERSATION);
472         var text = '';
474         if (!existingConversations.length) {
475             // If we didn't have any conversations then we need to show
476             // the content of the list and hide the empty message.
477             var listRoot = LazyLoadList.getRoot(root);
478             LazyLoadList.showContent(listRoot);
479             LazyLoadList.hideEmptyMessage(listRoot);
480         }
482         var messageCount = conversation.messages.length;
483         var lastMessage = messageCount ? conversation.messages[messageCount - 1] : null;
485         if (lastMessage) {
486             text = $(lastMessage.text).text() || lastMessage.text;
487         }
489         var formattedConversation = {
490             id: conversation.id,
491             name: conversation.name,
492             subname: conversation.subname,
493             lastmessagedate: lastMessage ? lastMessage.timeCreated : null,
494             sentfromcurrentuser: lastMessage ? lastMessage.fromLoggedInUser : null,
495             lastmessage: text,
496             imageurl: conversation.imageUrl,
497         };
499         // Cache the conversation.
500         loadedConversationsById[conversation.id] = conversation;
502         if (new Date().toDateString() == new Date(formattedConversation.lastmessagedate * 1000).toDateString()) {
503             formattedConversation.istoday = true;
504         }
506         return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: [formattedConversation]})
507             .then(function(html) {
508                 var contentContainer = LazyLoadList.getContentContainer(root);
509                 return contentContainer.prepend(html);
510             })
511             .then(function() {
512                 return incrementTotalConversationCount(root);
513             })
514             .catch(Notification.exception);
515     };
517     /**
518      * Delete a conversation from the list of conversations.
519      *
520      * @param  {Object} root Overview messages container element.
521      * @param  {Object} conversationElement The conversation element.
522      */
523     var deleteConversation = function(root, conversationElement) {
524         conversationElement.remove();
525         decrementTotalConversationCount(root);
527         var conversations = root.find(SELECTORS.CONVERSATION);
528         if (!conversations.length) {
529             // If we don't have any conversations then we need to hide
530             // the content of the list and show the empty message.
531             var listRoot = LazyLoadList.getRoot(root);
532             LazyLoadList.hideContent(listRoot);
533             LazyLoadList.showEmptyMessage(listRoot);
534         }
535     };
537     /**
538      * Mark a conversation as read.
539      *
540      * @param  {Object} root Overview messages container element.
541      * @param  {Object} conversationElement The conversation element.
542      */
543     var markConversationAsRead = function(root, conversationElement) {
544         var unreadCount = conversationElement.find(SELECTORS.UNREAD_COUNT);
545         unreadCount.text('0');
546         unreadCount.addClass('hidden');
547         decrementTotalUnreadConversationCount(root);
548     };
550     /**
551      * Listen to, and handle events in this section.
552      *
553      * @param {String} namespace Unique identifier for the Routes
554      * @param {Object} root The section container element.
555      * @param {Function} loadCallback The callback to load items.
556      * @param {Array|null} type The conversation types for this section
557      * @param {bool} includeFavourites If this section includes favourites
558      */
559     var registerEventListeners = function(namespace, root, loadCallback, types, includeFavourites) {
560         var listRoot = LazyLoadList.getRoot(root);
561         var conversationBelongsToThisSection = function(conversation) {
562             // Make sure the type is an int so that the index of check matches correctly.
563             var conversationType = parseInt(conversation.type, 10);
564             if (
565                 // If the conversation type isn't one this section cares about then we can ignore it.
566                 (types && types.indexOf(conversationType) < 0) ||
567                 // If this is the favourites section and the conversation isn't a favourite then ignore it.
568                 (includeFavourites && !conversation.isFavourite) ||
569                 // If this section doesn't include favourites and the conversation is a favourite then ignore it.
570                 (!includeFavourites && conversation.isFavourite)
571             ) {
572                 return false;
573             }
575             return true;
576         };
578         // Set the minimum height of the section to the height of the toggle. This
579         // smooths out the collapse animation.
580         var toggle = root.find(SELECTORS.TOGGLE);
581         root.css('min-height', toggle.outerHeight());
583         root.on('show.bs.collapse', function() {
584             setExpanded(root);
585             LazyLoadList.show(listRoot, loadCallback, render);
586         });
588         root.on('hidden.bs.collapse', function() {
589             setCollapsed(root);
590         });
592         PubSub.subscribe(MessageDrawerEvents.CONTACT_BLOCKED, function(userId) {
593             var conversationElement = getConversationElementFromUserId(root, userId);
594             if (conversationElement.length) {
595                 blockContact(conversationElement);
596             }
597         });
599         PubSub.subscribe(MessageDrawerEvents.CONTACT_UNBLOCKED, function(userId) {
600             var conversationElement = getConversationElementFromUserId(root, userId);
602             if (conversationElement.length) {
603                 unblockContact(conversationElement);
604             }
605         });
607         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_MUTED, function(conversation) {
608             var conversationId = conversation.id;
609             var conversationElement = getConversationElement(root, conversationId);
610             if (conversationElement.length) {
611                 muteConversation(conversationElement);
612             }
613         });
615         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_MUTED, function(conversation) {
616             var conversationId = conversation.id;
617             var conversationElement = getConversationElement(root, conversationId);
618             if (conversationElement.length) {
619                 unmuteConversation(conversationElement);
620             }
621         });
623         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, function(conversation) {
624             if (!conversationBelongsToThisSection(conversation)) {
625                 return;
626             }
628             var conversationId = conversation.id;
629             var element = getConversationElement(root, conversationId);
630             if (element.length) {
631                 updateLastMessage(element, conversation);
632             } else {
633                 createNewConversation(root, conversation);
634             }
635         });
637         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_DELETED, function(conversationId) {
638             var conversationElement = getConversationElement(root, conversationId);
639             if (conversationElement.length) {
640                 deleteConversation(root, conversationElement);
641             }
642         });
644         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, function(conversationId) {
645             var conversationElement = getConversationElement(root, conversationId);
646             if (conversationElement.length) {
647                 markConversationAsRead(root, conversationElement);
648             }
649         });
651         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_FAVOURITE, function(conversation) {
652             var conversationElement = null;
653             if (conversationBelongsToThisSection(conversation)) {
654                 conversationElement = getConversationElement(root, conversation.id);
655                 if (!conversationElement.length) {
656                     createNewConversation(root, conversation);
657                 }
658             } else {
659                 conversationElement = getConversationElement(root, conversation.id);
660                 if (conversationElement.length) {
661                     deleteConversation(root, conversationElement);
662                 }
663             }
664         });
666         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE, function(conversation) {
667             var conversationElement = null;
668             if (conversationBelongsToThisSection(conversation)) {
669                 conversationElement = getConversationElement(root, conversation.id);
670                 if (!conversationElement.length) {
671                     createNewConversation(root, conversation);
672                 }
673             } else {
674                 conversationElement = getConversationElement(root, conversation.id);
675                 if (conversationElement.length) {
676                     deleteConversation(root, conversationElement);
677                 }
678             }
679         });
681         CustomEvents.define(root, [CustomEvents.events.activate]);
682         root.on(CustomEvents.events.activate, SELECTORS.CONVERSATION, function(e, data) {
683             var conversationElement = $(e.target).closest(SELECTORS.CONVERSATION);
684             var conversationId = conversationElement.attr('data-conversation-id');
685             var conversation = loadedConversationsById[conversationId];
686             MessageDrawerRouter.go(namespace, MessageDrawerRoutes.VIEW_CONVERSATION, conversation);
688             data.originalEvent.preventDefault();
689         });
690     };
692     /**
693      * Setup the section.
694      *
695      * @param {String} namespace Unique identifier for the Routes
696      * @param {Object} header The header container element.
697      * @param {Object} body The section container element.
698      * @param {Object} footer The footer container element.
699      * @param {Array} types The conversation types that show in this section
700      * @param {bool} includeFavourites If this section includes favourites
701      * @param {Object} totalCountPromise Resolves wth the total conversations count
702      * @param {Object} unreadCountPromise Resolves wth the unread conversations count
703      */
704     var show = function(namespace, header, body, footer, types, includeFavourites, totalCountPromise, unreadCountPromise) {
705         var root = $(body);
707         if (!root.attr('data-init')) {
708             var loadCallback = getLoadCallback(types, includeFavourites, 0);
709             registerEventListeners(namespace, root, loadCallback, types, includeFavourites);
711             if (isVisible(root)) {
712                 setExpanded(root);
713                 var listRoot = LazyLoadList.getRoot(root);
714                 LazyLoadList.show(listRoot, loadCallback, render);
715             }
717             // This is given to us by the calling code because the total counts for all sections
718             // are loaded in a single ajax request rather than one request per section.
719             totalCountPromise.then(function(count) {
720                 renderTotalCount(root, count);
721                 loadedTotalCounts = true;
722                 return;
723             })
724             .catch(function() {
725                 // Silently ignore if we can't updated the counts. No need to bother the user.
726             });
728             // This is given to us by the calling code because the unread counts for all sections
729             // are loaded in a single ajax request rather than one request per section.
730             unreadCountPromise.then(function(count) {
731                 renderUnreadCount(root, count);
732                 loadedUnreadCounts = true;
733                 return;
734             })
735             .catch(function() {
736                 // Silently ignore if we can't updated the counts. No need to bother the user.
737             });
739             root.attr('data-init', true);
740         }
741     };
743     return {
744         show: show,
745         isVisible: isVisible
746     };
747 });