a0ab83c79bb8821f95a1d292b27509cdaa70e209
[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  {Number} type The conversation type.
237      * @param  {bool} includeFavourites Include/exclude favourites.
238      * @param  {Number} offset Result offset
239      * @return {Function}
240      */
241     var getLoadCallback = function(type, includeFavourites, offset) {
242         return function(root, userId) {
243             return MessageRepository.getConversations(
244                     userId,
245                     type,
246                     LOAD_LIMIT + 1,
247                     offset,
248                     includeFavourites,
249                     true // Always merge self-conversations with private conversations, to display them together.
250                 )
251                 .then(function(response) {
252                     var conversations = response.conversations;
254                     if (conversations.length > LOAD_LIMIT) {
255                         conversations = conversations.slice(0, -1);
256                     } else {
257                         LazyLoadList.setLoadedAll(root, true);
258                     }
260                     offset = offset + LOAD_LIMIT;
262                     conversations.forEach(function(conversation) {
263                         loadedConversationsById[conversation.id] = conversation;
264                     });
266                     return conversations;
267                 })
268                 .catch(Notification.exception);
269         };
270     };
272     /**
273      * Get the total count container element.
274      *
275      * @param  {Object} root Overview messages container element.
276      * @return {Object} Total count container element.
277      */
278     var getTotalConversationCountElement = function(root) {
279         return root.find(SELECTORS.SECTION_TOTAL_COUNT);
280     };
282     /**
283      * Get the unread conversations count container element.
284      *
285      * @param  {Object} root Overview messages container element.
286      * @return {Object} Unread conversations count container element.
287      */
288     var getTotalUnreadConversationCountElement = function(root) {
289         return root.find(SELECTORS.SECTION_UNREAD_COUNT);
290     };
292     /**
293      * Increment the total conversations count.
294      *
295      * @param  {Object} root Overview messages container element.
296      */
297     var incrementTotalConversationCount = function(root) {
298         if (loadedTotalCounts) {
299             var element = getTotalConversationCountElement(root);
300             var count = parseInt(element.text());
301             count = count + 1;
302             element.text(count);
303         }
304     };
306     /**
307      * Decrement the total conversations count.
308      *
309      * @param  {Object} root Overview messages container element.
310      */
311     var decrementTotalConversationCount = function(root) {
312         if (loadedTotalCounts) {
313             var element = getTotalConversationCountElement(root);
314             var count = parseInt(element.text());
315             count = count - 1;
316             element.text(count);
317         }
318     };
320     /**
321      * Decrement the total unread conversations count.
322      *
323      * @param  {Object} root Overview messages container element.
324      */
325     var decrementTotalUnreadConversationCount = function(root) {
326         if (loadedUnreadCounts) {
327             var element = getTotalUnreadConversationCountElement(root);
328             var count = parseInt(element.text());
329             count = count - 1;
330             element.text(count);
332             if (count < 1) {
333                 element.addClass('hidden');
334             }
335         }
336     };
338     /**
339      * Get a contact / conversation element.
340      *
341      * @param  {Object} root Overview messages container element.
342      * @param  {Number} conversationId The conversation id.
343      * @return {Object} Conversation element.
344      */
345     var getConversationElement = function(root, conversationId) {
346         return root.find('[data-conversation-id="' + conversationId + '"]');
347     };
349     /**
350      * Get a contact / conversation element from a user id.
351      *
352      * @param  {Object} root Overview messages container element.
353      * @param  {Number} userId The user id.
354      * @return {Object} Conversation element.
355      */
356     var getConversationElementFromUserId = function(root, userId) {
357         return root.find('[data-user-id="' + userId + '"]');
358     };
360     /**
361      * Show the conversation is muted icon.
362      *
363      * @param  {Object} conversationElement The conversation element.
364      */
365     var muteConversation = function(conversationElement) {
366         conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).removeClass('hidden');
367     };
369     /**
370      * Hide the conversation is muted icon.
371      *
372      * @param  {Object} conversationElement The conversation element.
373      */
374     var unmuteConversation = function(conversationElement) {
375         conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).addClass('hidden');
376     };
378     /**
379      * Show the contact is blocked icon.
380      *
381      * @param  {Object} conversationElement The conversation element.
382      */
383     var blockContact = function(conversationElement) {
384         conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).removeClass('hidden');
385     };
387     /**
388      * Hide the contact is blocked icon.
389      *
390      * @param  {Object} conversationElement The conversation element.
391      */
392     var unblockContact = function(conversationElement) {
393         conversationElement.find(SELECTORS.BLOCKED_ICON_CONTAINER).addClass('hidden');
394     };
396     /**
397      * Update the last message from / to a contact.
398      *
399      * @param  {Object} element Conversation element.
400      * @param  {Object} conversation The conversation.
401      * @return {Object} jQuery promise
402      */
403     var updateLastMessage = function(element, conversation) {
404         var message = conversation.messages[conversation.messages.length - 1];
405         var senderString = '';
406         var senderStringRequest;
407         if (message.fromLoggedInUser) {
408             senderStringRequest = {key: 'you', component: 'core_message'};
409         } else {
410             senderStringRequest = {key: 'sender', component: 'core_message', param: message.userFrom.fullname};
411         }
413         var stringRequests = [
414             senderStringRequest,
415             {key: 'strftimetime24', component: 'core_langconfig'},
416         ];
417         return Str.get_strings(stringRequests)
418             .then(function(strings) {
419                 senderString = strings[0];
420                 return UserDate.get([{timestamp: message.timeCreated, format: strings[1]}]);
421             })
422             .then(function(dates) {
423                 return dates[0];
424             })
425             .then(function(dateString) {
426                 element.find(SELECTORS.LAST_MESSAGE_DATE).text(dateString).removeClass('hidden');
428                 // No need to show sender string for private conversations and where the last message didn't come from you.
429                 if (!message.fromLoggedInUser &&
430                         conversation.type === MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
431                     senderString = '';
432                 }
434                 // Now load the last message.
435                 var lastMessage = senderString + " <span class='text-muted'>" + $(message.text).text() + "</span>";
437                 return element.find(SELECTORS.LAST_MESSAGE).html(lastMessage);
438             });
439     };
441     /**
442      * Create an render new conversation element in the list of conversations.
443      *
444      * @param  {Object} root Overview messages container element.
445      * @param  {Object} conversation The conversation.
446      * @return {Object} jQuery promise
447      */
448     var createNewConversation = function(root, conversation) {
449         var existingConversations = root.find(SELECTORS.CONVERSATION);
450         var text = '';
452         if (!existingConversations.length) {
453             // If we didn't have any conversations then we need to show
454             // the content of the list and hide the empty message.
455             var listRoot = LazyLoadList.getRoot(root);
456             LazyLoadList.showContent(listRoot);
457             LazyLoadList.hideEmptyMessage(listRoot);
458         }
460         var messageCount = conversation.messages.length;
461         var lastMessage = messageCount ? conversation.messages[messageCount - 1] : null;
463         if (lastMessage) {
464             text = $(lastMessage.text).text() || lastMessage.text;
465         }
467         var formattedConversation = {
468             id: conversation.id,
469             name: conversation.name,
470             subname: conversation.subname,
471             lastmessagedate: lastMessage ? lastMessage.timeCreated : null,
472             sentfromcurrentuser: lastMessage ? lastMessage.fromLoggedInUser : null,
473             lastmessage: text,
474             imageurl: conversation.imageUrl,
475         };
477         // Cache the conversation.
478         loadedConversationsById[conversation.id] = conversation;
480         if (new Date().toDateString() == new Date(formattedConversation.lastmessagedate * 1000).toDateString()) {
481             formattedConversation.istoday = true;
482         }
484         return Templates.render(TEMPLATES.CONVERSATIONS_LIST, {conversations: [formattedConversation]})
485             .then(function(html) {
486                 var contentContainer = LazyLoadList.getContentContainer(root);
487                 return contentContainer.prepend(html);
488             })
489             .then(function() {
490                 return incrementTotalConversationCount(root);
491             })
492             .catch(Notification.exception);
493     };
495     /**
496      * Delete a conversation from the list of conversations.
497      *
498      * @param  {Object} root Overview messages container element.
499      * @param  {Object} conversationElement The conversation element.
500      */
501     var deleteConversation = function(root, conversationElement) {
502         conversationElement.remove();
503         decrementTotalConversationCount(root);
505         var conversations = root.find(SELECTORS.CONVERSATION);
506         if (!conversations.length) {
507             // If we don't have any conversations then we need to hide
508             // the content of the list and show the empty message.
509             var listRoot = LazyLoadList.getRoot(root);
510             LazyLoadList.hideContent(listRoot);
511             LazyLoadList.showEmptyMessage(listRoot);
512         }
513     };
515     /**
516      * Mark a conversation as read.
517      *
518      * @param  {Object} root Overview messages container element.
519      * @param  {Object} conversationElement The conversation element.
520      */
521     var markConversationAsRead = function(root, conversationElement) {
522         var unreadCount = conversationElement.find(SELECTORS.UNREAD_COUNT);
523         unreadCount.text('0');
524         unreadCount.addClass('hidden');
525         decrementTotalUnreadConversationCount(root);
526     };
528     /**
529      * Listen to, and handle events in this section.
530      *
531      * @param {String} namespace Unique identifier for the Routes
532      * @param {Object} root The section container element.
533      * @param {Function} loadCallback The callback to load items.
534      * @param {Number} type The conversation type for this section
535      * @param {bool} includeFavourites If this section includes favourites
536      */
537     var registerEventListeners = function(namespace, root, loadCallback, type, includeFavourites) {
538         var listRoot = LazyLoadList.getRoot(root);
540         // Set the minimum height of the section to the height of the toggle. This
541         // smooths out the collapse animation.
542         var toggle = root.find(SELECTORS.TOGGLE);
543         root.css('min-height', toggle.outerHeight());
545         root.on('show.bs.collapse', function() {
546             setExpanded(root);
547             LazyLoadList.show(listRoot, loadCallback, render);
548         });
550         root.on('hidden.bs.collapse', function() {
551             setCollapsed(root);
552         });
554         PubSub.subscribe(MessageDrawerEvents.CONTACT_BLOCKED, function(userId) {
555             var conversationElement = getConversationElementFromUserId(root, userId);
556             if (conversationElement.length) {
557                 blockContact(conversationElement);
558             }
559         });
561         PubSub.subscribe(MessageDrawerEvents.CONTACT_UNBLOCKED, function(userId) {
562             var conversationElement = getConversationElementFromUserId(root, userId);
564             if (conversationElement.length) {
565                 unblockContact(conversationElement);
566             }
567         });
569         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_MUTED, function(conversation) {
570             var conversationId = conversation.id;
571             var conversationElement = getConversationElement(root, conversationId);
572             if (conversationElement.length) {
573                 muteConversation(conversationElement);
574             }
575         });
577         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_MUTED, function(conversation) {
578             var conversationId = conversation.id;
579             var conversationElement = getConversationElement(root, conversationId);
580             if (conversationElement.length) {
581                 unmuteConversation(conversationElement);
582             }
583         });
585         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, function(conversation) {
586             // Self-conversations could be displayed as private conversations when they are not starred. So we need to exclude
587             // them from the following check to make sure last messages are updated properly for them.
588             if (
589                 (type && conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF &&
590                 type != MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE && !conversation.isFavourite) ||
591                 (type && conversation.type != MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF &&
592                 type != conversation.type) ||
593                 (includeFavourites && !conversation.isFavourite) ||
594                 (!includeFavourites && conversation.isFavourite)
595             ) {
596                 return;
597             }
599             var conversationId = conversation.id;
600             var element = getConversationElement(root, conversationId);
601             if (element.length) {
602                 updateLastMessage(element, conversation);
603             } else {
604                 createNewConversation(root, conversation);
605             }
606         });
608         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_DELETED, function(conversationId) {
609             var conversationElement = getConversationElement(root, conversationId);
610             if (conversationElement.length) {
611                 deleteConversation(root, conversationElement);
612             }
613         });
615         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_READ, function(conversationId) {
616             var conversationElement = getConversationElement(root, conversationId);
617             if (conversationElement.length) {
618                 markConversationAsRead(root, conversationElement);
619             }
620         });
622         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_FAVOURITE, function(conversation) {
623             var conversationElement = null;
624             if (includeFavourites && (!type || type == conversation.type)) {
625                 conversationElement = getConversationElement(root, conversation.id);
626                 if (!conversationElement.length) {
627                     createNewConversation(root, conversation);
628                 }
629             } else if (type == conversation.type ||
630                     (type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE &&
631                      conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF)) {
632                 // Self-conversations are displayed in the private conversations section, so they should be removed from
633                 // there when they are favourited.
634                 conversationElement = getConversationElement(root, conversation.id);
635                 if (conversationElement.length) {
636                     deleteConversation(root, conversationElement);
637                 }
638             }
639         });
641         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE, function(conversation) {
642             var conversationElement = null;
643             if (includeFavourites) {
644                 conversationElement = getConversationElement(root, conversation.id);
645                 if (conversationElement.length) {
646                     deleteConversation(root, conversationElement);
647                 }
648             } else if (type == conversation.type ||
649                     (type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE &&
650                      conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF)) {
651                 // Self-conversations are displayed in the private conversations section, so they should be added
652                 // there when they are unfavourited.
653                 conversationElement = getConversationElement(root, conversation.id);
654                 if (!conversationElement.length) {
655                     createNewConversation(root, conversation);
656                 }
657             }
658         });
660         CustomEvents.define(root, [CustomEvents.events.activate]);
661         root.on(CustomEvents.events.activate, SELECTORS.CONVERSATION, function(e, data) {
662             var conversationElement = $(e.target).closest(SELECTORS.CONVERSATION);
663             var conversationId = conversationElement.attr('data-conversation-id');
664             var conversation = loadedConversationsById[conversationId];
665             MessageDrawerRouter.go(namespace, MessageDrawerRoutes.VIEW_CONVERSATION, conversation);
667             data.originalEvent.preventDefault();
668         });
669     };
671     /**
672      * Setup the section.
673      *
674      * @param {String} namespace Unique identifier for the Routes
675      * @param {Object} header The header container element.
676      * @param {Object} body The section container element.
677      * @param {Object} footer The footer container element.
678      * @param {Number} type The conversation type for this section
679      * @param {bool} includeFavourites If this section includes favourites
680      * @param {Object} totalCountPromise Resolves wth the total conversations count
681      * @param {Object} unreadCountPromise Resolves wth the unread conversations count
682      */
683     var show = function(namespace, header, body, footer, type, includeFavourites, totalCountPromise, unreadCountPromise) {
684         var root = $(body);
686         if (!root.attr('data-init')) {
687             var loadCallback = getLoadCallback(type, includeFavourites, 0);
688             registerEventListeners(namespace, root, loadCallback, type, includeFavourites);
690             if (isVisible(root)) {
691                 setExpanded(root);
692                 var listRoot = LazyLoadList.getRoot(root);
693                 LazyLoadList.show(listRoot, loadCallback, render);
694             }
696             // This is given to us by the calling code because the total counts for all sections
697             // are loaded in a single ajax request rather than one request per section.
698             totalCountPromise.then(function(count) {
699                 renderTotalCount(root, count);
700                 loadedTotalCounts = true;
701                 return;
702             })
703             .catch(function() {
704                 // Silently ignore if we can't updated the counts. No need to bother the user.
705             });
707             // This is given to us by the calling code because the unread counts for all sections
708             // are loaded in a single ajax request rather than one request per section.
709             unreadCountPromise.then(function(count) {
710                 renderUnreadCount(root, count);
711                 loadedUnreadCounts = true;
712                 return;
713             })
714             .catch(function() {
715                 // Silently ignore if we can't updated the counts. No need to bother the user.
716             });
718             root.attr('data-init', true);
719         }
720     };
722     return {
723         show: show,
724         isVisible: isVisible
725     };
726 });