8aafcce3b561f3b7ce085b761ec7ee6f21dc5f37
[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      * Reformat the conversations to a common standard because this is linked directly to the ajax response and via
163      * an event publish which operate on the same fields but in a different format
164      * @param conversations
165      */
166     var formatConversationsForRender = function(conversations) {
167         // Convert the conversation to the standard stored and then cache the conversation.
168         return conversations.map(function(conversation) {
169             return Object.keys(conversation).reduce(function(carry, key){
170                 if ($.isArray(conversation[key])) {
171                     carry[key.toLowerCase()] = formatConversationsForRender(conversation[key]);
172                 } else {
173                     carry[key.toLowerCase()] = conversation[key];
174                 }
176                 return carry;
177             }, {});
178         }, []);
179     };
181     /**
182      * Render the messages in the overview page.
183      *
184      * @param {Object} contentContainer Conversations content container.
185      * @param {Array} conversations List of conversations to render.
186      * @param {Number} userId Logged in user id.
187      * @return {Object} jQuery promise.
188      */
189     var render = function(conversations, userId) {
190         conversations = formatConversationsForRender(conversations);
191         var formattedConversations = conversations.map(function(conversation) {
193             var lastMessage = conversation.messages.length ? conversation.messages[conversation.messages.length - 1] : null;
195             var formattedConversation = {
196                 id: conversation.id,
197                 imageurl: conversation.imageurl,
198                 name: conversation.name,
199                 subname: conversation.subname,
200                 unreadcount: conversation.unreadcount,
201                 ismuted: conversation.ismuted,
202                 lastmessagedate: lastMessage ? lastMessage.timecreated : null,
203                 sentfromcurrentuser: lastMessage ? lastMessage.useridfrom == userId : null,
204                 lastmessage: lastMessage ? $(lastMessage.text).text() || lastMessage.text : null
205             };
207             var otherUser = null;
208             if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF) {
209                 // Self-conversations have only one member.
210                 otherUser = conversation.members[0];
211             } else if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
212                 // For private conversations, remove the current userId from the members to get the other user.
213                 otherUser = conversation.members.reduce(function(carry, member) {
214                     if (!carry && member.id != userId) {
215                         carry = member;
216                     }
217                     return carry;
218                 }, null);
219             }
221             if (otherUser !== null) {
222                 formattedConversation.userid = otherUser.id;
223                 formattedConversation.showonlinestatus = otherUser.showonlinestatus;
224                 formattedConversation.isonline = otherUser.isonline;
225                 formattedConversation.isblocked = otherUser.isblocked;
226             }
228             if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PUBLIC) {
229                 formattedConversation.lastsendername = conversation.members.reduce(function(carry, member) {
230                     if (!carry && member.id == lastMessage.useridfrom) {
231                         carry = member.fullname;
232                     }
233                     return carry;
234                 }, null);
235             }
237             return formattedConversation;
238         });
240         formattedConversations.forEach(function(conversation) {
241             if (new Date().toDateString() == new Date(conversation.lastmessagedate * 1000).toDateString()) {
242                 conversation.istoday = true;
243             }
244         });
246         return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: formattedConversations});
247     };
249     /**
250      * Build the callback to load conversations.
251      *
252      * @param  {Array|null} types The conversation types for this section.
253      * @param  {bool} includeFavourites Include/exclude favourites.
254      * @param  {Number} offset Result offset
255      * @return {Function}
256      */
257     var getLoadCallback = function(types, includeFavourites, offset) {
258         // Note: This function is a bit messy because we've added the concept of loading
259         // multiple conversations types (e.g. private + self) at once but haven't properly
260         // updated the web service to accept an array of types. Instead we've added a new
261         // parameter for the self type which means we can only ever load self + other type.
262         // This should be improved to make it more extensible in the future. Adding new params
263         // for each type isn't very scalable.
264         var type = null;
265         // Include self conversations in the results by default.
266         var includeSelfConversations = true;
267         if (types && types.length) {
268             // Just get the conversation types that aren't "self" for now.
269             var nonSelfConversationTypes = types.filter(function(candidate) {
270                 return candidate != MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF;
271             });
272             // If we're specifically asking for a list of types that doesn't include the self
273             // conversations then we don't need to include them.
274             includeSelfConversations = types.length != nonSelfConversationTypes.length;
275             // As mentioned above the webservice is currently limited to loading one type at a
276             // time (plus self conversations) so let's hope we never change this.
277             type = nonSelfConversationTypes[0];
278         }
280         return function(root, userId) {
281             return MessageRepository.getConversations(
282                     userId,
283                     type,
284                     LOAD_LIMIT + 1,
285                     offset,
286                     includeFavourites,
287                     includeSelfConversations
288                 )
289                 .then(function(response) {
290                     var conversations = response.conversations;
292                     if (conversations.length > LOAD_LIMIT) {
293                         conversations = conversations.slice(0, -1);
294                     } else {
295                         LazyLoadList.setLoadedAll(root, true);
296                     }
298                     offset = offset + LOAD_LIMIT;
300                     conversations.forEach(function(conversation) {
301                         loadedConversationsById[conversation.id] = conversation;
302                     });
304                     return conversations;
305                 })
306                 .catch(Notification.exception);
307         };
308     };
310     /**
311      * Get the total count container element.
312      *
313      * @param  {Object} root Overview messages container element.
314      * @return {Object} Total count container element.
315      */
316     var getTotalConversationCountElement = function(root) {
317         return root.find(SELECTORS.SECTION_TOTAL_COUNT);
318     };
320     /**
321      * Get the unread conversations count container element.
322      *
323      * @param  {Object} root Overview messages container element.
324      * @return {Object} Unread conversations count container element.
325      */
326     var getTotalUnreadConversationCountElement = function(root) {
327         return root.find(SELECTORS.SECTION_UNREAD_COUNT);
328     };
330     /**
331      * Increment the total conversations count.
332      *
333      * @param  {Object} root Overview messages container element.
334      */
335     var incrementTotalConversationCount = function(root) {
336         if (loadedTotalCounts) {
337             var element = getTotalConversationCountElement(root);
338             var count = parseInt(element.text());
339             count = count + 1;
340             element.text(count);
341         }
342     };
344     /**
345      * Decrement the total conversations count.
346      *
347      * @param  {Object} root Overview messages container element.
348      */
349     var decrementTotalConversationCount = function(root) {
350         if (loadedTotalCounts) {
351             var element = getTotalConversationCountElement(root);
352             var count = parseInt(element.text());
353             count = count - 1;
354             element.text(count);
355         }
356     };
358     /**
359      * Decrement the total unread conversations count.
360      *
361      * @param  {Object} root Overview messages container element.
362      */
363     var decrementTotalUnreadConversationCount = function(root) {
364         if (loadedUnreadCounts) {
365             var element = getTotalUnreadConversationCountElement(root);
366             var count = parseInt(element.text());
367             count = count - 1;
368             element.text(count);
370             if (count < 1) {
371                 element.addClass('hidden');
372             }
373         }
374     };
376     /**
377      * Get a contact / conversation element.
378      *
379      * @param  {Object} root Overview messages container element.
380      * @param  {Number} conversationId The conversation id.
381      * @return {Object} Conversation element.
382      */
383     var getConversationElement = function(root, conversationId) {
384         return root.find('[data-conversation-id="' + conversationId + '"]');
385     };
387     /**
388      * Get a contact / conversation element from a user id.
389      *
390      * @param  {Object} root Overview messages container element.
391      * @param  {Number} userId The user id.
392      * @return {Object} Conversation element.
393      */
394     var getConversationElementFromUserId = function(root, userId) {
395         return root.find('[data-user-id="' + userId + '"]');
396     };
398     /**
399      * Show the conversation is muted icon.
400      *
401      * @param  {Object} conversationElement The conversation element.
402      */
403     var muteConversation = function(conversationElement) {
404         conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).removeClass('hidden');
405     };
407     /**
408      * Hide the conversation is muted icon.
409      *
410      * @param  {Object} conversationElement The conversation element.
411      */
412     var unmuteConversation = function(conversationElement) {
413         conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).addClass('hidden');
414     };
416     /**
417      * Show the contact is blocked icon.
418      *
419      * @param  {Object} conversationElement The conversation element.
420      */
421     var blockContact = function(conversationElement) {
422         conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).removeClass('hidden');
423     };
425     /**
426      * Hide the contact is blocked icon.
427      *
428      * @param  {Object} conversationElement The conversation element.
429      */
430     var unblockContact = function(conversationElement) {
431         conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).addClass('hidden');
432     };
434     /**
435      * Create an render new conversation element in the list of conversations.
436      *
437      * @param  {Object} root Overview messages container element.
438      * @param  {Object} conversation The conversation.
439      * @return {Object} jQuery promise
440      */
441     var createNewConversation = function(root, conversation) {
442         var existingConversations = root.find(SELECTORS.CONVERSATION);
443         var text = '';
445         if (!existingConversations.length) {
446             // If we didn't have any conversations then we need to show
447             // the content of the list and hide the empty message.
448             var listRoot = LazyLoadList.getRoot(root);
449             LazyLoadList.showContent(listRoot);
450             LazyLoadList.hideEmptyMessage(listRoot);
451         }
453         var messageCount = conversation.messages.length;
454         var lastMessage = messageCount ? conversation.messages[messageCount - 1] : null;
456         if (lastMessage) {
457             text = $(lastMessage.text).text() || lastMessage.text;
458             conversation.messages[messageCount - 1].useridfrom = lastMessage.userFrom.id;
459         }
461         var formattedConversation = {
462             id: conversation.id,
463             name: conversation.name,
464             subname: conversation.subname,
465             lastmessagedate: lastMessage ? lastMessage.timeCreated : null,
466             sentfromcurrentuser: lastMessage ? lastMessage.fromLoggedInUser : null,
467             lastmessage: text,
468             imageurl: conversation.imageUrl,
469         };
471         // Convert the conversation to the standard stored and then cache the conversation.
472         loadedConversationsById[conversation.id] = formatConversationsForRender([conversation])[0];
474         if (new Date().toDateString() == new Date(formattedConversation.lastmessagedate * 1000).toDateString()) {
475             formattedConversation.istoday = true;
476         }
478         return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: [formattedConversation]})
479             .then(function(html) {
480                 var contentContainer = LazyLoadList.getContentContainer(root);
481                 return contentContainer.prepend(html);
482             })
483             .then(function() {
484                 return incrementTotalConversationCount(root);
485             })
486             .catch(Notification.exception);
487     };
489     /**
490      * Delete a conversation from the list of conversations.
491      *
492      * @param  {Object} root Overview messages container element.
493      * @param  {Object} conversationElement The conversation element.
494      */
495     var deleteConversation = function(root, conversationElement) {
496         conversationElement.remove();
497         decrementTotalConversationCount(root);
499         var conversations = root.find(SELECTORS.CONVERSATION);
500         if (!conversations.length) {
501             // If we don't have any conversations then we need to hide
502             // the content of the list and show the empty message.
503             var listRoot = LazyLoadList.getRoot(root);
504             LazyLoadList.hideContent(listRoot);
505             LazyLoadList.showEmptyMessage(listRoot);
506         }
507     };
509     /**
510      * Mark a conversation as read.
511      *
512      * @param  {Object} root Overview messages container element.
513      * @param  {Object} conversationElement The conversation element.
514      */
515     var markConversationAsRead = function(root, conversationElement) {
516         var unreadCount = conversationElement.find(SELECTORS.UNREAD_COUNT);
517         unreadCount.text('0');
518         unreadCount.addClass('hidden');
519         decrementTotalUnreadConversationCount(root);
520     };
522     /**
523      * Listen to, and handle events in this section.
524      *
525      * @param {String} namespace Unique identifier for the Routes
526      * @param {Object} root The section container element.
527      * @param {Function} loadCallback The callback to load items.
528      * @param {Array|null} types The conversation types for this section
529      * @param {bool} includeFavourites If this section includes favourites
530      * @param {String} fromPanel Routing argument to send if the section is loaded in message index left panel.
531      */
532     var registerEventListeners = function(namespace, root, loadCallback, types, includeFavourites, fromPanel) {
533         var listRoot = LazyLoadList.getRoot(root);
534         var conversationBelongsToThisSection = function(conversation) {
535             // Make sure the type is an int so that the index of check matches correctly.
536             var conversationType = parseInt(conversation.type, 10);
537             if (
538                 // If the conversation type isn't one this section cares about then we can ignore it.
539                 (types && types.indexOf(conversationType) < 0) ||
540                 // If this is the favourites section and the conversation isn't a favourite then ignore it.
541                 (includeFavourites && !conversation.isFavourite) ||
542                 // If this section doesn't include favourites and the conversation is a favourite then ignore it.
543                 (!includeFavourites && conversation.isFavourite)
544             ) {
545                 return false;
546             }
548             return true;
549         };
551         // Set the minimum height of the section to the height of the toggle. This
552         // smooths out the collapse animation.
553         var toggle = root.find(SELECTORS.TOGGLE);
554         root.css('min-height', toggle.outerHeight());
556         root.on('show.bs.collapse', function() {
557             setExpanded(root);
558             LazyLoadList.show(listRoot, loadCallback, function(contentContainer, conversations, userId) {
559                 return render(conversations, userId).then(function(html) {
560                     contentContainer.append(html);
561                     return html;
562                 });
563             });
564         });
566         root.on('hidden.bs.collapse', function() {
567             setCollapsed(root);
568         });
570         PubSub.subscribe(MessageDrawerEvents.CONTACT_BLOCKED, function(userId) {
571             var conversationElement = getConversationElementFromUserId(root, userId);
572             if (conversationElement.length) {
573                 blockContact(conversationElement);
574             }
575         });
577         PubSub.subscribe(MessageDrawerEvents.CONTACT_UNBLOCKED, function(userId) {
578             var conversationElement = getConversationElementFromUserId(root, userId);
580             if (conversationElement.length) {
581                 unblockContact(conversationElement);
582             }
583         });
585         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_MUTED, function(conversation) {
586             var conversationId = conversation.id;
587             var conversationElement = getConversationElement(root, conversationId);
588             if (conversationElement.length) {
589                 muteConversation(conversationElement);
590             }
591         });
593         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_MUTED, function(conversation) {
594             var conversationId = conversation.id;
595             var conversationElement = getConversationElement(root, conversationId);
596             if (conversationElement.length) {
597                 unmuteConversation(conversationElement);
598             }
599         });
601         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, function(conversation) {
602             if (!conversationBelongsToThisSection(conversation)) {
603                 return;
604             }
606             var conversationId = conversation.id;
607             var element = getConversationElement(root, conversationId);
608             if (element.length) {
609                 var contentContainer = LazyLoadList.getContentContainer(root);
610                 render([conversation], conversation.loggedInUserId)
611                     .then(function(html) {
612                             contentContainer.prepend(html);
613                             element.remove();
614                             return html;
615                         })
616                     .catch(Notification.exception);
617             } else {
618                 createNewConversation(root, conversation);
619             }
620         });
622         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_DELETED, function(conversationId) {
623             var conversationElement = getConversationElement(root, conversationId);
624             delete loadedConversationsById[conversationId];
625             if (conversationElement.length) {
626                 deleteConversation(root, conversationElement);
627             }
628         });
630         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, function(conversationId) {
631             var conversationElement = getConversationElement(root, conversationId);
632             if (conversationElement.length) {
633                 markConversationAsRead(root, conversationElement);
634             }
635         });
637         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_FAVOURITE, function(conversation) {
638             var conversationElement = null;
639             if (conversationBelongsToThisSection(conversation)) {
640                 conversationElement = getConversationElement(root, conversation.id);
641                 if (!conversationElement.length) {
642                     createNewConversation(root, conversation);
643                 }
644             } else {
645                 conversationElement = getConversationElement(root, conversation.id);
646                 if (conversationElement.length) {
647                     deleteConversation(root, conversationElement);
648                 }
649             }
650         });
652         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE, function(conversation) {
653             var conversationElement = null;
654             if (conversationBelongsToThisSection(conversation)) {
655                 conversationElement = getConversationElement(root, conversation.id);
656                 if (!conversationElement.length) {
657                     createNewConversation(root, conversation);
658                 }
659             } else {
660                 conversationElement = getConversationElement(root, conversation.id);
661                 if (conversationElement.length) {
662                     deleteConversation(root, conversationElement);
663                 }
664             }
665         });
667         CustomEvents.define(root, [CustomEvents.events.activate]);
668         root.on(CustomEvents.events.activate, SELECTORS.CONVERSATION, function(e, data) {
669             var conversationElement = $(e.target).closest(SELECTORS.CONVERSATION);
670             var conversationId = conversationElement.attr('data-conversation-id');
671             var conversation = loadedConversationsById[conversationId];
672             MessageDrawerRouter.go(namespace, MessageDrawerRoutes.VIEW_CONVERSATION, conversation, fromPanel);
674             data.originalEvent.preventDefault();
675         });
676     };
678     /**
679      * Setup the section.
680      *
681      * @param {String} namespace Unique identifier for the Routes
682      * @param {Object} header The header container element.
683      * @param {Object} body The section container element.
684      * @param {Object} footer The footer container element.
685      * @param {Array} types The conversation types that show in this section
686      * @param {bool} includeFavourites If this section includes favourites
687      * @param {Object} totalCountPromise Resolves wth the total conversations count
688      * @param {Object} unreadCountPromise Resolves wth the unread conversations count
689      * @param {bool} fromPanel shown in message app panel.
690      */
691     var show = function(namespace, header, body, footer, types, includeFavourites, totalCountPromise, unreadCountPromise,
692         fromPanel) {
693         var root = $(body);
695         if (!root.attr('data-init')) {
696             var loadCallback = getLoadCallback(types, includeFavourites, 0);
697             registerEventListeners(namespace, root, loadCallback, types, includeFavourites, fromPanel);
699             if (isVisible(root)) {
700                 setExpanded(root);
701                 var listRoot = LazyLoadList.getRoot(root);
702                 LazyLoadList.show(listRoot, loadCallback, function(contentContainer, conversations, userId) {
703                     return render(conversations, userId).then(function(html) {
704                         contentContainer.append(html);
705                         return html;
706                     });
707                 });
708             }
710             // This is given to us by the calling code because the total counts for all sections
711             // are loaded in a single ajax request rather than one request per section.
712             totalCountPromise.then(function(count) {
713                 renderTotalCount(root, count);
714                 loadedTotalCounts = true;
715                 return;
716             })
717             .catch(function() {
718                 // Silently ignore if we can't updated the counts. No need to bother the user.
719             });
721             // This is given to us by the calling code because the unread counts for all sections
722             // are loaded in a single ajax request rather than one request per section.
723             unreadCountPromise.then(function(count) {
724                 renderUnreadCount(root, count);
725                 loadedUnreadCounts = true;
726                 return;
727             })
728             .catch(function() {
729                 // Silently ignore if we can't updated the counts. No need to bother the user.
730             });
732             root.attr('data-init', true);
733         }
734     };
736     return {
737         show: show,
738         isVisible: isVisible
739     };
740 });