MDL-64045 message: stop favouriting null conversations
[moodle.git] / message / amd / src / message_drawer_view_conversation_renderer.js
blob5e5cf9083556828f23b40ea8d1b1eaac0a15e04b
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.
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  * This module updates the UI for the conversation page in the message
18  * drawer.
19  *
20  * The module will take a patch from the message_drawer_view_conversation_patcher
21  * module and update the UI to reflect the changes.
22  *
23  * This is the only module that ever modifies the UI of the conversation page.
24  *
25  * @module     core_message/message_drawer_view_conversation_renderer
26  * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
27  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28  */
29 define(
31     'jquery',
32     'core/notification',
33     'core/str',
34     'core/templates',
35     'core/user_date',
36     'core_message/message_drawer_view_conversation_constants'
38 function(
39     $,
40     Notification,
41     Str,
42     Templates,
43     UserDate,
44     Constants
45 ) {
46     var SELECTORS = Constants.SELECTORS;
47     var TEMPLATES = Constants.TEMPLATES;
48     var CONVERSATION_TYPES = Constants.CONVERSATION_TYPES;
50     /**
51      * Get the messages container element.
52      *
53      * @param  {Object} body Conversation body container element.
54      * @return {Object} The messages container element.
55      */
56     var getMessagesContainer = function(body) {
57         return body.find(SELECTORS.CONTENT_MESSAGES_CONTAINER);
58     };
60     /**
61      * Show the messages container element.
62      *
63      * @param  {Object} body Conversation body container element.
64      */
65     var showMessagesContainer = function(body) {
66         getMessagesContainer(body).removeClass('hidden');
67     };
69     /**
70      * Hide the messages container element.
71      *
72      * @param  {Object} body Conversation body container element.
73      */
74     var hideMessagesContainer = function(body) {
75         getMessagesContainer(body).addClass('hidden');
76     };
78     /**
79      * Get the contact request sent container element.
80      *
81      * @param  {Object} body Conversation body container element.
82      * @return {Object} The messages container element.
83      */
84     var getContactRequestSentContainer = function(body) {
85         return body.find(SELECTORS.CONTACT_REQUEST_SENT_MESSAGE_CONTAINER);
86     };
88     /**
89      * Hide the contact request sent container element.
90      *
91      * @param  {Object} body Conversation body container element.
92      * @return {Object} The messages container element.
93      */
94     var hideContactRequestSentContainer = function(body) {
95         return getContactRequestSentContainer(body).addClass('hidden');
96     };
98     /**
99      * Get the footer container element.
100      *
101      * @param  {Object} footer Conversation footer container element.
102      * @return {Object} The footer container element.
103      */
104     var getFooterContentContainer = function(footer) {
105         return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_CONTAINER);
106     };
108     /**
109      * Show the footer container element.
110      *
111      * @param  {Object} footer Conversation footer container element.
112      */
113     var showFooterContent = function(footer) {
114         getFooterContentContainer(footer).removeClass('hidden');
115     };
117     /**
118      * Hide the footer container element.
119      *
120      * @param  {Object} footer Conversation footer container element.
121      */
122     var hideFooterContent = function(footer) {
123         getFooterContentContainer(footer).addClass('hidden');
124     };
126     /**
127      * Get the footer edit mode container element.
128      *
129      * @param  {Object} footer Conversation footer container element.
130      * @return {Object} The footer container element.
131      */
132     var getFooterEditModeContainer = function(footer) {
133         return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_EDIT_MODE_CONTAINER);
134     };
136     /**
137      * Show the footer edit mode container element.
138      *
139      * @param  {Object} footer Conversation footer container element.
140      */
141     var showFooterEditMode = function(footer) {
142         getFooterEditModeContainer(footer).removeClass('hidden');
143     };
145     /**
146      * Hide the footer edit mode container element.
147      *
148      * @param  {Object} footer Conversation footer container element.
149      */
150     var hideFooterEditMode = function(footer) {
151         getFooterEditModeContainer(footer).addClass('hidden');
152     };
154     /**
155      * Get the footer placeholder.
156      *
157      * @param  {Object} footer Conversation footer container element.
158      * @return {Object} The footer placeholder container element.
159      */
160     var getFooterPlaceholderContainer = function(footer) {
161         return footer.find(SELECTORS.PLACEHOLDER_CONTAINER);
162     };
164     /**
165      * Show the footer placeholder
166      *
167      * @param  {Object} footer Conversation footer container element.
168      */
169     var showFooterPlaceholder = function(footer) {
170         getFooterPlaceholderContainer(footer).removeClass('hidden');
171     };
173     /**
174      * Hide the footer placeholder
175      *
176      * @param  {Object} footer Conversation footer container element.
177      */
178     var hideFooterPlaceholder = function(footer) {
179         getFooterPlaceholderContainer(footer).addClass('hidden');
180     };
182     /**
183      * Get the footer Require add as contact container element.
184      *
185      * @param  {Object} footer Conversation footer container element.
186      * @return {Object} The footer Require add as contact container element.
187      */
188     var getFooterRequireContactContainer = function(footer) {
189         return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_REQUIRE_CONTACT_CONTAINER);
190     };
192     /**
193      * Show the footer add as contact dialogue.
194      *
195      * @param  {Object} footer Conversation footer container element.
196      */
197     var showFooterRequireContact = function(footer) {
198         getFooterRequireContactContainer(footer).removeClass('hidden');
199     };
201     /**
202      * Hide the footer add as contact dialogue.
203      *
204      * @param  {Object} footer Conversation footer container element.
205      */
206     var hideFooterRequireContact = function(footer) {
207         getFooterRequireContactContainer(footer).addClass('hidden');
208     };
210     /**
211      * Get the footer Required to unblock contact container element.
212      *
213      * @param  {Object} footer Conversation footer container element.
214      * @return {Object} The footer Required to unblock contact container element.
215      */
216     var getFooterRequireUnblockContainer = function(footer) {
217         return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_REQUIRE_UNBLOCK_CONTAINER);
218     };
220     /**
221      * Show the footer Required to unblock contact container element.
222      *
223      * @param  {Object} footer Conversation footer container element.
224      */
225     var showFooterRequireUnblock = function(footer) {
226         getFooterRequireUnblockContainer(footer).removeClass('hidden');
227     };
229     /**
230      * Hide the footer Required to unblock contact container element.
231      *
232      * @param  {Object} footer Conversation footer container element.
233      */
234     var hideFooterRequireUnblock = function(footer) {
235         getFooterRequireUnblockContainer(footer).addClass('hidden');
236     };
238     /**
239      * Get the footer Unable to message contact container element.
240      *
241      * @param  {Object} footer Conversation footer container element.
242      * @return {Object} The footer Unable to message contact container element.
243      */
244     var getFooterUnableToMessageContainer = function(footer) {
245         return footer.find(SELECTORS.CONTENT_MESSAGES_FOOTER_UNABLE_TO_MESSAGE_CONTAINER);
246     };
248     /**
249      * Show the footer Unable to message contact container element.
250      *
251      * @param  {Object} footer Conversation footer container element.
252      */
253     var showFooterUnableToMessage = function(footer) {
254         getFooterUnableToMessageContainer(footer).removeClass('hidden');
255     };
257     /**
258      * Hide the footer Unable to message contact container element.
259      *
260      * @param  {Object} footer Conversation footer container element.
261      */
262     var hideFooterUnableToMessage = function(footer) {
263         getFooterUnableToMessageContainer(footer).addClass('hidden');
264     };
266     /**
267      * Hide all header elements.
268      *
269      * @param  {Object} header Conversation header container element.
270      */
271     var hideAllHeaderElements = function(header) {
272         hideHeaderContent(header);
273         hideHeaderEditMode(header);
274         hideHeaderPlaceholder(header);
275     };
277     /**
278      * Hide all footer dialogues and messages.
279      *
280      * @param  {Object} footer Conversation footer container element.
281      */
282     var hideAllFooterElements = function(footer) {
283         hideFooterContent(footer);
284         hideFooterEditMode(footer);
285         hideFooterPlaceholder(footer);
286         hideFooterRequireContact(footer);
287         hideFooterRequireUnblock(footer);
288         hideFooterUnableToMessage(footer);
289     };
291     /**
292      * Get the content placeholder container element.
293      *
294      * @param  {Object} body Conversation body container element.
295      * @return {Object} The body placeholder container element.
296      */
297     var getContentPlaceholderContainer = function(body) {
298         return body.find(SELECTORS.CONTENT_PLACEHOLDER_CONTAINER);
299     };
301     /**
302      * Show the content placeholder.
303      *
304      * @param  {Object} body Conversation body container element.
305      */
306     var showContentPlaceholder = function(body) {
307         getContentPlaceholderContainer(body).removeClass('hidden');
308     };
310     /**
311      * Hide the content placeholder.
312      *
313      * @param  {Object} body Conversation body container element.
314      */
315     var hideContentPlaceholder = function(body) {
316         getContentPlaceholderContainer(body).addClass('hidden');
317     };
319     /**
320      * Get the header content container element.
321      *
322      * @param  {Object} header Conversation header container element.
323      * @return {Object} The header content container element.
324      */
325     var getHeaderContent = function(header) {
326         return header.find(SELECTORS.HEADER);
327     };
329     /**
330      * Show the header content.
331      *
332      * @param  {Object} header Conversation header container element.
333      */
334     var showHeaderContent = function(header) {
335         getHeaderContent(header).removeClass('hidden');
336     };
338     /**
339      * Hide the header content.
340      *
341      * @param  {Object} header Conversation header container element.
342      */
343     var hideHeaderContent = function(header) {
344         getHeaderContent(header).addClass('hidden');
345     };
347     /**
348      * Get the header edit mode container element.
349      *
350      * @param  {Object} header Conversation header container element.
351      * @return {Object} The header content container element.
352      */
353     var getHeaderEditMode = function(header) {
354         return header.find(SELECTORS.HEADER_EDIT_MODE);
355     };
357     /**
358      * Show the header edit mode container.
359      *
360      * @param  {Object} header Conversation header container element.
361      */
362     var showHeaderEditMode = function(header) {
363         getHeaderEditMode(header).removeClass('hidden');
364     };
366     /**
367      * Hide the header edit mode container.
368      *
369      * @param  {Object} header Conversation header container element.
370      */
371     var hideHeaderEditMode = function(header) {
372         getHeaderEditMode(header).addClass('hidden');
373     };
375     /**
376      * Get the header placeholder container element.
377      *
378      * @param  {Object} header Conversation header container element.
379      * @return {Object} The header placeholder container element.
380      */
381     var getHeaderPlaceholderContainer = function(header) {
382         return header.find(SELECTORS.HEADER_PLACEHOLDER_CONTAINER);
383     };
385     /**
386      * Show the header placeholder.
387      *
388      * @param  {Object} header Conversation header container element.
389      */
390     var showHeaderPlaceholder = function(header) {
391         getHeaderPlaceholderContainer(header).removeClass('hidden');
392     };
394     /**
395      * Hide the header placeholder.
396      *
397      * @param  {Object} header Conversation header container element.
398      */
399     var hideHeaderPlaceholder = function(header) {
400         getHeaderPlaceholderContainer(header).addClass('hidden');
401     };
403     /**
404      * Get the text input area element.
405      *
406      * @param  {Object} footer Conversation footer container element.
407      * @return {Object} The footer placeholder container element.
408      */
409     var getMessageTextArea = function(footer) {
410         return footer.find(SELECTORS.MESSAGE_TEXT_AREA);
411     };
413     /**
414      * Get a message element.
415      *
416      * @param  {Object} body Conversation body container element.
417      * @param  {Number} messageId the Message id.
418      * @return {Object} A message element from the conversation.
419      */
420     var getMessageElement = function(body, messageId) {
421         var messagesContainer = getMessagesContainer(body);
422         return messagesContainer.find('[data-message-id="' + messageId + '"]');
423     };
425     /**
426      * Get the day container element. The day container element holds a list of messages for that day.
427      *
428      * @param  {Object} body Conversation body container element.
429      * @param  {Number} dayTimeCreated Midnight timestamp for the day.
430      * @return {Object} jQuery object
431      */
432     var getDayElement = function(body, dayTimeCreated) {
433         var messagesContainer = getMessagesContainer(body);
434         return messagesContainer.find('[data-day-id="' + dayTimeCreated + '"]');
435     };
437     /**
438      * Get the more messages loading icon container element.
439      *
440      * @param  {Object} body Conversation body container element.
441      * @return {Object} The more messages loading container element.
442      */
443     var getMoreMessagesLoadingIconContainer = function(body) {
444         return body.find(SELECTORS.MORE_MESSAGES_LOADING_ICON_CONTAINER);
445     };
447     /**
448      * Show the more messages loading icon.
449      *
450      * @param  {Object} body Conversation body container element.
451      */
452     var showMoreMessagesLoadingIcon = function(body) {
453         getMoreMessagesLoadingIconContainer(body).removeClass('hidden');
454     };
456     /**
457      * Hide the more messages loading icon.
458      *
459      * @param  {Object} body Conversation body container element.
460      */
461     var hideMoreMessagesLoadingIcon = function(body) {
462         getMoreMessagesLoadingIconContainer(body).addClass('hidden');
463     };
465     /**
466      * Disable the message controls for sending a message.
467      *
468      * @param  {Object} footer Conversation footer container element.
469      */
470     var disableSendMessage = function(footer) {
471         footer.find(SELECTORS.SEND_MESSAGE_BUTTON).prop('disabled', true);
472         getMessageTextArea(footer).prop('disabled', true);
473     };
475     /**
476      * Enable the message controls for sending a message.
477      *
478      * @param  {Object} footer Conversation footer container element.
479      */
480     var enableSendMessage = function(footer) {
481         footer.find(SELECTORS.SEND_MESSAGE_BUTTON).prop('disabled', false);
482         getMessageTextArea(footer).prop('disabled', false);
483     };
485     /**
486      * Show the sending message loading icon and disable sending more.
487      *
488      * @param  {Object} footer Conversation footer container element.
489      */
490     var startSendMessageLoading = function(footer) {
491         disableSendMessage(footer);
492         footer.find(SELECTORS.SEND_MESSAGE_ICON_CONTAINER).addClass('hidden');
493         footer.find(SELECTORS.LOADING_ICON_CONTAINER).removeClass('hidden');
494     };
496     /**
497      * Hide the sending message loading icon and allow sending new messages.
498      *
499      * @param  {Object} footer Conversation footer container element.
500      */
501     var stopSendMessageLoading = function(footer) {
502         enableSendMessage(footer);
503         footer.find(SELECTORS.SEND_MESSAGE_ICON_CONTAINER).removeClass('hidden');
504         footer.find(SELECTORS.LOADING_ICON_CONTAINER).addClass('hidden');
505     };
507     /**
508      * Clear out message text input and focus the input element.
509      *
510      * @param  {Object} footer Conversation footer container element.
511      */
512     var hasSentMessage = function(footer) {
513         var textArea = getMessageTextArea(footer);
514         textArea.val('');
515         textArea.focus();
516     };
518     /**
519      * Get the confirm dialogue container element.
520      *
521      * @param  {Object} root The container element to search.
522      * @return {Object} The confirm dialogue container element.
523      */
524     var getConfirmDialogueContainer = function(root) {
525         return root.find(SELECTORS.CONFIRM_DIALOGUE_CONTAINER);
526     };
528     /**
529      * Show the confirm dialogue container element.
530      *
531      * @param  {Object} root The container element containing a dialogue.
532      */
533     var showConfirmDialogueContainer = function(root) {
534         var container = getConfirmDialogueContainer(root);
535         var siblings = container.siblings(':not(.hidden)');
536         siblings.attr('aria-hidden', true);
537         siblings.attr('tabindex', -1);
538         siblings.attr('data-confirm-dialogue-hidden', true);
540         container.removeClass('hidden');
541     };
543     /**
544      * Hide the confirm dialogue container element.
545      *
546      * @param  {Object} root The container element containing a dialogue.
547      */
548     var hideConfirmDialogueContainer = function(root) {
549         var container = getConfirmDialogueContainer(root);
550         var siblings = container.siblings('[data-confirm-dialogue-hidden="true"]');
551         siblings.removeAttr('aria-hidden');
552         siblings.removeAttr('tabindex');
553         siblings.removeAttr('data-confirm-dialogue-hidden');
555         container.addClass('hidden');
556     };
558     /**
559      * Set the number of selected messages.
560      *
561      * @param {Object} header The header container element.
562      * @param {Number} value The new number to display.
563      */
564     var setMessagesSelectedCount = function(header, value) {
565         getHeaderEditMode(header).find(SELECTORS.MESSAGES_SELECTED_COUNT).text(value);
566     };
568     /**
569      * Format message for the mustache template, transform camelCase properties to lowercase properties.
570      *
571      * @param  {Array} messages Array of message objects.
572      * @param  {Object} datesCache Cache timestamps and their formatted date string.
573      * @return {Array} Messages formated for mustache template.
574      */
575     var formatMessagesForTemplate = function(messages, datesCache) {
576         return messages.map(function(message) {
577             return {
578                 id: message.id,
579                 isread: message.isRead,
580                 fromloggedinuser: message.fromLoggedInUser,
581                 userfrom: message.userFrom,
582                 text: message.text,
583                 formattedtime: datesCache[message.timeCreated]
584             };
585         });
586     };
588     /**
589      * Create rendering promises for each day containing messages.
590      *
591      * @param  {Object} header The header container element.
592      * @param  {Object} body The body container element.
593      * @param  {Object} footer The footer container element.
594      * @param  {Array} days Array of days containing messages.
595      * @param  {Object} datesCache Cache timestamps and their formatted date string.
596      * @return {Promise} Days rendering promises.
597      */
598     var renderAddDays = function(header, body, footer, days, datesCache) {
599         var messagesContainer = getMessagesContainer(body);
600         var daysRenderPromises = days.map(function(data) {
601             return Templates.render(TEMPLATES.DAY, {
602                 timestamp: data.value.timestamp,
603                 messages: formatMessagesForTemplate(data.value.messages, datesCache)
604             });
605         });
607         return $.when.apply($, daysRenderPromises).then(function() {
608             // Wait until all of the rendering is done for each of the days
609             // to ensure they are added to the page in the correct order.
610             days.forEach(function(data, index) {
611                 daysRenderPromises[index]
612                     .then(function(html) {
613                         if (data.before) {
614                             var element = getDayElement(body, data.before.timestamp);
615                             return $(html).insertBefore(element);
616                         } else {
617                             return messagesContainer.append(html);
618                         }
619                     })
620                     .catch(function() {
621                         // Fail silently.
622                     });
623             });
625             return;
626         });
627     };
629     /**
630      * Add (more) messages to day containers.
631      *
632      * @param  {Object} header The header container element.
633      * @param  {Object} body The body container element.
634      * @param  {Object} footer The footer container element.
635      * @param  {Array} messages List of messages.
636      * @param  {Object} datesCache Cache timestamps and their formatted date string.
637      * @return {Promise} Messages rendering promises.
638      */
639     var renderAddMessages = function(header, body, footer, messages, datesCache) {
640         var messagesData = messages.map(function(data) {
641             return data.value;
642         });
643         var formattedMessages = formatMessagesForTemplate(messagesData, datesCache);
645         return Templates.render(TEMPLATES.MESSAGES, {messages: formattedMessages})
646             .then(function(html) {
647                 var messageList = $(html);
648                 messages.forEach(function(data) {
649                     var messageHtml = messageList.find('[data-message-id="' + data.value.id + '"]');
650                     if (data.before) {
651                         var element = getMessageElement(body, data.before.id);
652                         return messageHtml.insertBefore(element);
653                     } else {
654                         var dayContainer = getDayElement(body, data.day.timestamp);
655                         var dayMessagesContainer = dayContainer.find(SELECTORS.DAY_MESSAGES_CONTAINER);
656                         return dayMessagesContainer.append(messageHtml);
657                     }
658                 });
660                 return;
661             });
662     };
664     /**
665      * Remove days from conversation.
666      *
667      * @param  {Object} body The body container element.
668      * @param  {Array} days Array of days to be removed.
669      */
670     var renderRemoveDays = function(body, days) {
671         days.forEach(function(data) {
672             getDayElement(body, data.timestamp).remove();
673         });
674     };
676     /**
677      * Remove messages from conversation.
678      *
679      * @param  {Object} body The body container element.
680      * @param  {Array} messages Array of messages to be removed.
681      */
682     var renderRemoveMessages = function(body, messages) {
683         messages.forEach(function(data) {
684             getMessageElement(body, data.id).remove();
685         });
686     };
688     /**
689      * Render the full conversation base on input from the statemanager.
690      *
691      * This will pre-load all of the formatted timestamps for each message that
692      * needs to render to reduce the number of networks requests.
693      *
694      * @param  {Object} header The header container element.
695      * @param  {Object} body The body container element.
696      * @param  {Object} footer The footer container element.
697      * @param  {Object} data The conversation diff.
698      * @return {Object} jQuery promise.
699      */
700     var renderConversation = function(header, body, footer, data) {
701         var renderingPromises = [];
702         var hasAddDays = data.days.add.length > 0;
703         var hasAddMessages = data.messages.add.length > 0;
704         var timestampsToFormat = [];
705         var datesCachePromise = $.Deferred().resolve({}).promise();
707         if (hasAddDays) {
708             // Search for all of the timeCreated values in all of the messages in all of
709             // the days that we need to render.
710             timestampsToFormat = timestampsToFormat.concat(data.days.add.reduce(function(carry, day) {
711                 return carry.concat(day.value.messages.map(function(message) {
712                     return message.timeCreated;
713                 }));
714             }, []));
715         }
717         if (hasAddMessages) {
718             // Search for all of the timeCreated values in all of the messages that we
719             // need to render.
720             timestampsToFormat = timestampsToFormat.concat(data.messages.add.map(function(message) {
721                 return message.value.timeCreated;
722             }));
723         }
725         if (timestampsToFormat.length) {
726             // If we have timestamps then pre-load the formatted version of each of them
727             // in a single request to the server. This saves the templates doing multiple
728             // individual requests.
729             datesCachePromise = Str.get_string('strftimetime24', 'core_langconfig')
730                 .then(function(format) {
731                     var requests = timestampsToFormat.map(function(timestamp) {
732                         return {
733                             timestamp: timestamp,
734                             format: format
735                         };
736                     });
738                     return UserDate.get(requests);
739                 })
740                 .then(function(formattedTimes) {
741                     return timestampsToFormat.reduce(function(carry, timestamp, index) {
742                         carry[timestamp] = formattedTimes[index];
743                         return carry;
744                     }, {});
745                 });
746         }
748         if (hasAddDays) {
749             renderingPromises.push(datesCachePromise.then(function(datesCache) {
750                 return renderAddDays(header, body, footer, data.days.add, datesCache);
751             }));
752         }
754         if (hasAddMessages) {
755             renderingPromises.push(datesCachePromise.then(function(datesCache) {
756                 return renderAddMessages(header, body, footer, data.messages.add, datesCache);
757             }));
758         }
760         if (data.days.remove.length > 0) {
761             renderRemoveDays(body, data.days.remove);
762         }
764         if (data.messages.remove.length > 0) {
765             renderRemoveMessages(body, data.messages.remove);
766         }
768         return $.when.apply($, renderingPromises);
769     };
771     /**
772      * Render the conversation header.
773      *
774      * @param {Object} header The header container element.
775      * @param {Object} body The body container element.
776      * @param {Object} footer The footer container element.
777      * @param {Object} data Data for header.
778      * @return {Object} jQuery promise
779      */
780     var renderHeader = function(header, body, footer, data) {
781         var headerContainer = getHeaderContent(header);
782         var template = TEMPLATES.HEADER_PUBLIC;
784         if (data.type == CONVERSATION_TYPES.PRIVATE) {
785             template = data.showControls ? TEMPLATES.HEADER_PRIVATE : TEMPLATES.HEADER_PRIVATE_NO_CONTROLS;
786         }
788         return Templates.render(template, data.context)
789             .then(function(html, js) {
790                 Templates.replaceNodeContents(headerContainer, html, js);
791                 return;
792             });
793     };
795     /**
796      * Render the conversation footer.
797      *
798      * @param {Object} header The header container element.
799      * @param {Object} body The body container element.
800      * @param {Object} footer The footer container element.
801      * @param {Object} data Data for footer.
802      * @return {Object} jQuery promise.
803      */
804     var renderFooter = function(header, body, footer, data) {
805         hideAllFooterElements(footer);
807         switch (data.type) {
808             case 'placeholder':
809                 return showFooterPlaceholder(footer);
810             case 'add-contact':
811                 return Str.get_strings([
812                         {
813                             key: 'requirecontacttomessage',
814                             component: 'core_message',
815                             param: data.user.fullname
816                         },
817                         {
818                             key: 'isnotinyourcontacts',
819                             component: 'core_message',
820                             param: data.user.fullname
821                         }
822                     ])
823                     .then(function(strings) {
824                         var title = strings[1];
825                         var text = strings[0];
826                         var footerContainer = getFooterRequireContactContainer(footer);
827                         footerContainer.find(SELECTORS.TITLE).text(title);
828                         footerContainer.find(SELECTORS.TEXT).text(text);
829                         showFooterRequireContact(footer);
830                         return strings;
831                     });
832             case 'edit-mode':
833                 return showFooterEditMode(footer);
834             case 'content':
835                 return showFooterContent(footer);
836             case 'unblock':
837                 return showFooterRequireUnblock(footer);
838             case 'unable-to-message':
839                 return showFooterUnableToMessage(footer);
840         }
842         return true;
843     };
845     /**
846      * Scroll to a message in the conversation.
847      *
848      * @param {Object} header The header container element.
849      * @param {Object} body The body container element.
850      * @param {Object} footer The footer container element.
851      * @param {Number} messageId Message id.
852      */
853     var renderScrollToMessage = function(header, body, footer, messageId) {
854         var messagesContainer = getMessagesContainer(body);
855         var messageElement = getMessageElement(body, messageId);
856         var position = messageElement.position();
857         // Scroll the message container down to the top of the message element.
858         if (position) {
859             var scrollTop = messagesContainer.scrollTop() + position.top;
860             messagesContainer.scrollTop(scrollTop);
861         }
862     };
864     /**
865      * Hide or show the conversation header.
866      *
867      * @param {Object} header The header container element.
868      * @param {Object} body The body container element.
869      * @param {Object} footer The footer container element.
870      * @param {Bool} isLoadingMembers Members loading.
871      */
872     var renderLoadingMembers = function(header, body, footer, isLoadingMembers) {
873         if (isLoadingMembers) {
874             hideHeaderContent(header);
875             showHeaderPlaceholder(header);
876         } else {
877             showHeaderContent(header);
878             hideHeaderPlaceholder(header);
879         }
880     };
882     /**
883      * Hide or show loading conversation messages.
884      *
885      * @param {Object} header The header container element.
886      * @param {Object} body The body container element.
887      * @param {Object} footer The footer container element.
888      * @param {Bool} isLoadingFirstMessages Messages loading.
889      */
890     var renderLoadingFirstMessages = function(header, body, footer, isLoadingFirstMessages) {
891         if (isLoadingFirstMessages) {
892             hideMessagesContainer(body);
893             showContentPlaceholder(body);
894         } else {
895             showMessagesContainer(body);
896             hideContentPlaceholder(body);
897         }
898     };
900     /**
901      * Hide or show loading more messages.
902      *
903      * @param {Object} header The header container element.
904      * @param {Object} body The body container element.
905      * @param {Object} footer The footer container element.
906      * @param {Bool} isLoading Messages loading.
907      */
908     var renderLoadingMessages = function(header, body, footer, isLoading) {
909         if (isLoading) {
910             showMoreMessagesLoadingIcon(body);
911         } else {
912             hideMoreMessagesLoadingIcon(body);
913         }
914     };
916     /**
917      * Activate or deactivate send message controls.
918      *
919      * @param {Object} header The header container element.
920      * @param {Object} body The body container element.
921      * @param {Object} footer The footer container element.
922      * @param {Bool} isSending Message sending.
923      */
924     var renderSendingMessage = function(header, body, footer, isSending) {
925         if (isSending) {
926             startSendMessageLoading(footer);
927         } else {
928             stopSendMessageLoading(footer);
929             hasSentMessage(footer);
930         }
931     };
933     /**
934      * Show a confirmation dialogue
935      *
936      * @param {Object} header The header container element.
937      * @param {Object} body The body container element.
938      * @param {Object} footer The footer container element.
939      * @param {String} buttonSelectors Selectors for the buttons to show.
940      * @param {String} bodyText Text to show in dialogue.
941      * @param {String} headerText Text to show in dialogue header.
942      * @param {Bool} canCancel Can this dialogue be cancelled.
943      * @param {Bool} skipHeader Skip blanking out the header
944      */
945     var showConfirmDialogue = function(
946         header,
947         body,
948         footer,
949         buttonSelectors,
950         bodyText,
951         headerText,
952         canCancel,
953         skipHeader
954     ) {
955         var dialogue = getConfirmDialogueContainer(body);
956         var buttons = buttonSelectors.map(function(selector) {
957             return dialogue.find(selector);
958         });
959         var cancelButton = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_CANCEL_BUTTON);
960         var text = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_TEXT);
961         var dialogueHeader = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_HEADER);
963         dialogue.find('button').addClass('hidden');
965         if (canCancel) {
966             cancelButton.removeClass('hidden');
967         } else {
968             cancelButton.addClass('hidden');
969         }
971         if (headerText) {
972             dialogueHeader.removeClass('hidden');
973             dialogueHeader.text(headerText);
974         } else {
975             dialogueHeader.addClass('hidden');
976             dialogueHeader.text('');
977         }
979         buttons.forEach(function(button) {
980             button.removeClass('hidden');
981         });
982         text.text(bodyText);
983         showConfirmDialogueContainer(footer);
984         showConfirmDialogueContainer(body);
986         if (!skipHeader) {
987             showConfirmDialogueContainer(header);
988         }
990         dialogue.find(SELECTORS.CAN_RECEIVE_FOCUS).filter(':visible').first().focus();
991     };
993     /**
994      * Hide the dialogue
995      *
996      * @param {Object} header The header container element.
997      * @param {Object} body The body container element.
998      * @param {Object} footer The footer container element.
999      * @return {Bool} always true.
1000      */
1001     var hideConfirmDialogue = function(header, body, footer) {
1002         var dialogue = getConfirmDialogueContainer(body);
1003         var cancelButton = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_CANCEL_BUTTON);
1004         var text = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_TEXT);
1005         var dialogueHeader = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_HEADER);
1007         hideConfirmDialogueContainer(body);
1008         hideConfirmDialogueContainer(footer);
1009         hideConfirmDialogueContainer(header);
1010         dialogue.find('button').addClass('hidden');
1011         cancelButton.removeClass('hidden');
1012         text.text('');
1013         dialogueHeader.addClass('hidden');
1014         dialogueHeader.text('');
1016         header.find(SELECTORS.CAN_RECEIVE_FOCUS).first().focus();
1017         return true;
1018     };
1020     /**
1021      * Render the confirm block user dialogue.
1022      *
1023      * @param {Object} header The header container element.
1024      * @param {Object} body The body container element.
1025      * @param {Object} footer The footer container element.
1026      * @param {Object} user User to block.
1027      * @return {Object} jQuery promise
1028      */
1029     var renderConfirmBlockUser = function(header, body, footer, user) {
1030         if (user) {
1031             return Str.get_string('blockuserconfirm', 'core_message', user.fullname)
1032                 .then(function(string) {
1033                     return showConfirmDialogue(header, body, footer, [SELECTORS.ACTION_CONFIRM_BLOCK], string, '', true, false);
1034                 });
1035         } else {
1036             return hideConfirmDialogue(header, body, footer);
1037         }
1038     };
1040     /**
1041      * Render the confirm unblock user dialogue.
1042      *
1043      * @param {Object} header The header container element.
1044      * @param {Object} body The body container element.
1045      * @param {Object} footer The footer container element.
1046      * @param {Object} user User to unblock.
1047      * @return {Object} jQuery promise
1048      */
1049     var renderConfirmUnblockUser = function(header, body, footer, user) {
1050         if (user) {
1051             return Str.get_string('unblockuserconfirm', 'core_message', user.fullname)
1052                 .then(function(string) {
1053                     return showConfirmDialogue(header, body, footer, [SELECTORS.ACTION_CONFIRM_UNBLOCK], string, '', true, false);
1054                 });
1055         } else {
1056             return hideConfirmDialogue(header, body, footer);
1057         }
1058     };
1060     /**
1061      * Render the add user as contact dialogue.
1062      *
1063      * @param {Object} header The header container element.
1064      * @param {Object} body The body container element.
1065      * @param {Object} footer The footer container element.
1066      * @param {Object} user User to add as contact.
1067      * @return {Object} jQuery promise
1068      */
1069     var renderConfirmAddContact = function(header, body, footer, user) {
1070         if (user) {
1071             return Str.get_string('addcontactconfirm', 'core_message', user.fullname)
1072                 .then(function(string) {
1073                     return showConfirmDialogue(
1074                         header,
1075                         body,
1076                         footer,
1077                         [SELECTORS.ACTION_CONFIRM_ADD_CONTACT],
1078                         string,
1079                         '',
1080                         true,
1081                         false
1082                     );
1083                 });
1084         } else {
1085             return hideConfirmDialogue(header, body, footer);
1086         }
1087     };
1089     /**
1090      * Render the remove user from contacts dialogue.
1091      *
1092      * @param {Object} header The header container element.
1093      * @param {Object} body The body container element.
1094      * @param {Object} footer The footer container element.
1095      * @param {Object} user User to remove from contacts.
1096      * @return {Object} jQuery promise
1097      */
1098     var renderConfirmRemoveContact = function(header, body, footer, user) {
1099         if (user) {
1100             return Str.get_string('removecontactconfirm', 'core_message', user.fullname)
1101                 .then(function(string) {
1102                     return showConfirmDialogue(
1103                         header,
1104                         body,
1105                         footer,
1106                         [SELECTORS.ACTION_CONFIRM_REMOVE_CONTACT],
1107                         string,
1108                         '',
1109                         true,
1110                         false
1111                     );
1112                 });
1113         } else {
1114             return hideConfirmDialogue(header, body, footer);
1115         }
1116     };
1118     /**
1119      * Render the delete selected messages dialogue.
1120      *
1121      * @param {Object} header The header container element.
1122      * @param {Object} body The body container element.
1123      * @param {Object} footer The footer container element.
1124      * @param {Bool} show If the dialogue should show.
1125      * @return {Object} jQuery promise
1126      */
1127     var renderConfirmDeleteSelectedMessages = function(header, body, footer, show) {
1128         if (show) {
1129             return Str.get_string('deleteselectedmessagesconfirm', 'core_message')
1130                 .then(function(string) {
1131                     return showConfirmDialogue(
1132                         header,
1133                         body,
1134                         footer,
1135                         [SELECTORS.ACTION_CONFIRM_DELETE_SELECTED_MESSAGES],
1136                         string,
1137                         '',
1138                         true,
1139                         false
1140                     );
1141                 });
1142         } else {
1143             return hideConfirmDialogue(header, body, footer);
1144         }
1145     };
1147     /**
1148      * Render the confirm delete conversation dialogue.
1149      *
1150      * @param {Object} header The header container element.
1151      * @param {Object} body The body container element.
1152      * @param {Object} footer The footer container element.
1153      * @param {Bool} show If the dialogue should show
1154      * @return {Object} jQuery promise
1155      */
1156     var renderConfirmDeleteConversation = function(header, body, footer, show) {
1157         if (show) {
1158             return Str.get_string('deleteallconfirm', 'core_message')
1159                 .then(function(string) {
1160                     return showConfirmDialogue(
1161                         header,
1162                         body,
1163                         footer,
1164                         [SELECTORS.ACTION_CONFIRM_DELETE_CONVERSATION],
1165                         string,
1166                         '',
1167                         true,
1168                         false
1169                     );
1170                 });
1171         } else {
1172             return hideConfirmDialogue(header, body, footer);
1173         }
1174     };
1176     /**
1177      * Render the confirm delete conversation dialogue.
1178      *
1179      * @param {Object} header The header container element.
1180      * @param {Object} body The body container element.
1181      * @param {Object} footer The footer container element.
1182      * @param {Bool} user The other user object.
1183      * @return {Object} jQuery promise
1184      */
1185     var renderConfirmContactRequest = function(header, body, footer, user) {
1186         if (user) {
1187             return Str.get_string('userwouldliketocontactyou', 'core_message', user.fullname)
1188                 .then(function(string) {
1189                     var buttonSelectors = [
1190                         SELECTORS.ACTION_ACCEPT_CONTACT_REQUEST,
1191                         SELECTORS.ACTION_DECLINE_CONTACT_REQUEST
1192                     ];
1193                     return showConfirmDialogue(header, body, footer, buttonSelectors, string, '', false, true);
1194                 });
1195         } else {
1196             return hideConfirmDialogue(header, body, footer);
1197         }
1198     };
1200     /**
1201      * Show or hide the block / unblock option in the header dropdown menu.
1202      *
1203      * @param {Object} header The header container element.
1204      * @param {Object} body The body container element.
1205      * @param {Object} footer The footer container element.
1206      * @param {Bool} isBlocked is user blocked.
1207      */
1208     var renderIsBlocked = function(header, body, footer, isBlocked) {
1209         if (isBlocked) {
1210             header.find(SELECTORS.ACTION_REQUEST_BLOCK).addClass('hidden');
1211             header.find(SELECTORS.ACTION_REQUEST_UNBLOCK).removeClass('hidden');
1212         } else {
1213             header.find(SELECTORS.ACTION_REQUEST_BLOCK).removeClass('hidden');
1214             header.find(SELECTORS.ACTION_REQUEST_UNBLOCK).addClass('hidden');
1215         }
1216     };
1218     /**
1219      * Show or hide the favourite / unfavourite option in the header dropdown menu
1220      * and the favourite star in the header title.
1221      *
1222      * @param {Object} header The header container element.
1223      * @param {Object} body The body container element.
1224      * @param {Object} footer The footer container element.
1225      * @param {Bool} isFavourite is this conversation a favourite.
1226      */
1227     var renderIsFavourite = function(header, body, footer, state) {
1228         var favouriteIcon = header.find(SELECTORS.FAVOURITE_ICON_CONTAINER);
1229         var addFavourite = header.find(SELECTORS.ACTION_CONFIRM_FAVOURITE);
1230         var removeFavourite = header.find(SELECTORS.ACTION_CONFIRM_UNFAVOURITE);
1232         switch (state) {
1233             case 'hide':
1234                 favouriteIcon.addClass('hidden');
1235                 addFavourite.addClass('hidden');
1236                 removeFavourite.addClass('hidden');
1237                 break;
1238             case 'show-add':
1239                 favouriteIcon.addClass('hidden');
1240                 addFavourite.removeClass('hidden');
1241                 removeFavourite.addClass('hidden');
1242                 break;
1243             case 'show-remove':
1244                 favouriteIcon.removeClass('hidden');
1245                 addFavourite.addClass('hidden');
1246                 removeFavourite.removeClass('hidden');
1247                 break;
1248         }
1249     };
1251     /**
1252      * Show or hide the add / remove user as contact option in the header dropdown menu.
1253      *
1254      * @param {Object} header The header container element.
1255      * @param {Object} body The body container element.
1256      * @param {Object} footer The footer container element.
1257      * @param {Bool} state the contact state.
1258      */
1259     var renderIsContact = function(header, body, footer, state) {
1260         var addContact = header.find(SELECTORS.ACTION_REQUEST_ADD_CONTACT);
1261         var removeContact = header.find(SELECTORS.ACTION_REQUEST_REMOVE_CONTACT);
1263         switch (state) {
1264             case 'pending-contact':
1265                 addContact.addClass('hidden');
1266                 removeContact.addClass('hidden');
1267                 break;
1268             case 'contact':
1269                 addContact.addClass('hidden');
1270                 removeContact.removeClass('hidden');
1271                 break;
1272             case 'non-contact':
1273                 addContact.removeClass('hidden');
1274                 removeContact.addClass('hidden');
1275                 break;
1276         }
1277     };
1279     /**
1280      * Show or hide confirm action from confirm dialogue is loading.
1281      *
1282      * @param {Object} header The header container element.
1283      * @param {Object} body The body container element.
1284      * @param {Object} footer The footer container element.
1285      * @param {Bool} isLoading confirm action is loading.
1286      */
1287     var renderLoadingConfirmAction = function(header, body, footer, isLoading) {
1288         var dialogue = getConfirmDialogueContainer(body);
1289         var buttons = dialogue.find('button');
1290         var buttonText = dialogue.find(SELECTORS.CONFIRM_DIALOGUE_BUTTON_TEXT);
1291         var loadingIcon = dialogue.find(SELECTORS.LOADING_ICON_CONTAINER);
1293         if (isLoading) {
1294             buttons.prop('disabled', true);
1295             buttonText.addClass('hidden');
1296             loadingIcon.removeClass('hidden');
1297         } else {
1298             buttons.prop('disabled', false);
1299             buttonText.removeClass('hidden');
1300             loadingIcon.addClass('hidden');
1301         }
1302     };
1304     /**
1305      * Show or hide the header and footer content for edit mode.
1306      *
1307      * @param {Object} header The header container element.
1308      * @param {Object} body The body container element.
1309      * @param {Object} footer The footer container element.
1310      * @param {Bool} inEditMode In edit mode or not.
1311      */
1312     var renderInEditMode = function(header, body, footer, inEditMode) {
1313         var messages = null;
1315         if (inEditMode) {
1316             messages = body.find(SELECTORS.MESSAGE_NOT_SELECTED);
1317             messages.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).removeClass('hidden');
1318             hideHeaderContent(header);
1319             showHeaderEditMode(header);
1320         } else {
1321             messages = getMessagesContainer(body);
1322             messages.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).addClass('hidden');
1323             messages.find(SELECTORS.MESSAGE_SELECTED_ICON).addClass('hidden');
1324             showHeaderContent(header);
1325             hideHeaderEditMode(header);
1326         }
1327     };
1329     /**
1330      * Select or unselect messages.
1331      *
1332      * @param {Object} header The header container element.
1333      * @param {Object} body The body container element.
1334      * @param {Object} footer The footer container element.
1335      * @param {Object} data The messages to select or unselect.
1336      */
1337     var renderSelectedMessages = function(header, body, footer, data) {
1338         var hasSelectedMessages = data.count > 0;
1340         if (data.add.length) {
1341             data.add.forEach(function(messageId) {
1342                 var message = getMessageElement(body, messageId);
1343                 message.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).addClass('hidden');
1344                 message.find(SELECTORS.MESSAGE_SELECTED_ICON).removeClass('hidden');
1345                 message.attr('aria-checked', true);
1346             });
1347         }
1349         if (data.remove.length) {
1350             data.remove.forEach(function(messageId) {
1351                 var message = getMessageElement(body, messageId);
1353                 if (hasSelectedMessages) {
1354                     message.find(SELECTORS.MESSAGE_NOT_SELECTED_ICON).removeClass('hidden');
1355                 }
1357                 message.find(SELECTORS.MESSAGE_SELECTED_ICON).addClass('hidden');
1358                 message.attr('aria-checked', false);
1359             });
1360         }
1362         setMessagesSelectedCount(header, data.count);
1363     };
1365     /**
1366      * Show or hide the require add contact panel.
1367      *
1368      * @param {Object} header The header container element.
1369      * @param {Object} body The body container element.
1370      * @param {Object} footer The footer container element.
1371      * @param {Object} data Whether the user has to be added a a contact.
1372      * @return {Object} jQuery promise
1373      */
1374     var renderRequireAddContact = function(header, body, footer, data) {
1375         if (data.show && !data.hasMessages) {
1376             return Str.get_strings([
1377                     {
1378                         key: 'requirecontacttomessage',
1379                         component: 'core_message',
1380                         param: data.user.fullname
1381                     },
1382                     {
1383                         key: 'isnotinyourcontacts',
1384                         component: 'core_message',
1385                         param: data.user.fullname
1386                     }
1387                 ])
1388                 .then(function(strings) {
1389                     var title = strings[1];
1390                     var text = strings[0];
1391                     return showConfirmDialogue(
1392                         header,
1393                         body,
1394                         footer,
1395                         [SELECTORS.ACTION_REQUEST_ADD_CONTACT],
1396                         text,
1397                         title,
1398                         false,
1399                         true
1400                     );
1401                 });
1402         } else {
1403             return hideConfirmDialogue(header, body, footer);
1404         }
1405     };
1407     /**
1408      * Show or hide the require add contact panel.
1409      *
1410      * @param {Object} header The header container element.
1411      * @param {Object} body The body container element.
1412      * @param {Object} footer The footer container element.
1413      * @param {Object} userFullName Full name of the other user.
1414      * @return {Object|true} jQuery promise
1415      */
1416     var renderContactRequestSent = function(header, body, footer, userFullName) {
1417         var container = getContactRequestSentContainer(body);
1418         if (userFullName) {
1419             return Str.get_string('yourcontactrequestpending', 'core_message', userFullName)
1420                 .then(function(string) {
1421                     container.find(SELECTORS.TEXT).text(string);
1422                     container.removeClass('hidden');
1423                     return string;
1424                 });
1425         } else {
1426             container.addClass('hidden');
1427             return true;
1428         }
1429     };
1431     /**
1432      * Reset the UI to the initial state.
1433      *
1434      * @param {Object} header The header container element.
1435      * @param {Object} body The body container element.
1436      * @param {Object} footer The footer container element.
1437      * @return {Bool}
1438      */
1439     var renderReset = function(header, body, footer) {
1440         hideConfirmDialogue(header, body, footer);
1441         hideContactRequestSentContainer(body);
1442         hideAllHeaderElements(header);
1443         showHeaderPlaceholder(header);
1444         hideAllFooterElements(footer);
1445         showFooterPlaceholder(footer);
1446         return true;
1447     };
1449     var render = function(header, body, footer, patch) {
1450         var configs = [
1451             {
1452                 // Resetting the UI needs to come first, if it's required.
1453                 reset: renderReset
1454             },
1455             {
1456                 // Any async rendering (stuff that requires templates, strings etc) should
1457                 // go in here.
1458                 conversation: renderConversation,
1459                 header: renderHeader,
1460                 footer: renderFooter,
1461                 confirmBlockUser: renderConfirmBlockUser,
1462                 confirmUnblockUser: renderConfirmUnblockUser,
1463                 confirmAddContact: renderConfirmAddContact,
1464                 confirmRemoveContact: renderConfirmRemoveContact,
1465                 confirmDeleteSelectedMessages: renderConfirmDeleteSelectedMessages,
1466                 confirmDeleteConversation: renderConfirmDeleteConversation,
1467                 confirmContactRequest: renderConfirmContactRequest,
1468                 requireAddContact: renderRequireAddContact,
1469                 contactRequestSent: renderContactRequestSent
1470             },
1471             {
1472                 loadingMembers: renderLoadingMembers,
1473                 loadingFirstMessages: renderLoadingFirstMessages,
1474                 loadingMessages: renderLoadingMessages,
1475                 sendingMessage: renderSendingMessage,
1476                 isBlocked: renderIsBlocked,
1477                 isContact: renderIsContact,
1478                 isFavourite: renderIsFavourite,
1479                 loadingConfirmAction: renderLoadingConfirmAction,
1480                 inEditMode: renderInEditMode
1481             },
1482             {
1483                 // Scrolling should be last to make sure everything
1484                 // on the page is visible.
1485                 scrollToMessage: renderScrollToMessage,
1486                 selectedMessages: renderSelectedMessages
1487             }
1488         ];
1489         // Helper function to process each of the configs above.
1490         var processConfig = function(config) {
1491             var results = [];
1493             for (var key in patch) {
1494                 if (config.hasOwnProperty(key)) {
1495                     var renderFunc = config[key];
1496                     var patchValue = patch[key];
1497                     results.push(renderFunc(header, body, footer, patchValue));
1498                 }
1499             }
1501             return results;
1502         };
1504         // The first config is special because it resets the UI.
1505         var renderingPromises = processConfig(configs[0]);
1506         // The second config is special because it contains async rendering.
1507         renderingPromises = renderingPromises.concat(processConfig(configs[1]));
1509         // Wait for the async rendering to complete before processing the
1510         // rest of the configs, in order.
1511         return $.when.apply($, renderingPromises)
1512             .then(function() {
1513                 for (var i = 2; i < configs.length; i++) {
1514                     processConfig(configs[i]);
1515                 }
1517                 return;
1518             })
1519             .catch(Notification.exception);
1520     };
1522     return {
1523         render: render,
1524     };