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