MDL-65896 admin: add admin setting to show emoji picker
[moodle.git] / message / amd / src / message_drawer_view_conversation.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 the conversation page in the message drawer.
18  *
19  * This function handles all of the user actions that the user can take
20  * when interacting with the conversation page.
21  *
22  * It maintains a view state which is a data representation of the view
23  * and only operates on that data.
24  *
25  * The view state is immutable and should never be modified directly. Instead
26  * all changes to the view state should be done using the StateManager which
27  * will generate a new version of the view state with the requested changes.
28  *
29  * After any changes to the view state the module will call the render function
30  * to ask the renderer to update the UI.
31  *
32  * General rules for this module:
33  * 1.) Never modify viewState directly. All changes should be via the StateManager.
34  * 2.) Call render() with the new state when you want to update the UI
35  * 3.) Never modify the UI directly in this module. This module is only concerned
36  *     with the data in the view state.
37  *
38  * The general flow for a user interaction will be something like:
39  * User interaction: User clicks "confirm block" button to block the other user
40  *      1.) This module is hears the click
41  *      2.) This module sends a request to the server to block the user
42  *      3.) The server responds with the new user profile
43  *      4.) This module generates a new state using the StateManager with the updated
44  *          user profile.
45  *      5.) This module asks the Patcher to generate a patch from the current state and
46  *          the newly generated state. This patch tells the renderer what has changed
47  *          between the states.
48  *      6.) This module gives the Renderer the generated patch. The renderer updates
49  *          the UI with changes according to the patch.
50  *
51  * @module     core_message/message_drawer_view_conversation
52  * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
53  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
54  */
55 define(
56 [
57     'jquery',
58     'core/auto_rows',
59     'core/backoff_timer',
60     'core/custom_interaction_events',
61     'core/notification',
62     'core/pubsub',
63     'core/str',
64     'core_message/message_repository',
65     'core_message/message_drawer_events',
66     'core_message/message_drawer_view_conversation_constants',
67     'core_message/message_drawer_view_conversation_patcher',
68     'core_message/message_drawer_view_conversation_renderer',
69     'core_message/message_drawer_view_conversation_state_manager',
70     'core_message/message_drawer_router',
71     'core_message/message_drawer_routes',
72     'core/emoji/auto_complete',
73     'core/emoji/picker'
74 ],
75 function(
76     $,
77     AutoRows,
78     BackOffTimer,
79     CustomEvents,
80     Notification,
81     PubSub,
82     Str,
83     Repository,
84     MessageDrawerEvents,
85     Constants,
86     Patcher,
87     Renderer,
88     StateManager,
89     MessageDrawerRouter,
90     MessageDrawerRoutes,
91     initialiseEmojiAutoComplete,
92     initialiseEmojiPicker
93 ) {
95     // Contains a cache of all view states that have been loaded so far
96     // which saves us having to reload stuff with network requests when
97     // switching between conversations.
98     var stateCache = {};
99     // The current data representation of the view.
100     var viewState = null;
101     var loadedAllMessages = false;
102     var messagesOffset = 0;
103     var newMessagesPollTimer = null;
104     var isRendering = false;
105     var renderBuffer = [];
106     // If the UI is currently resetting.
107     var isResetting = true;
108     // If the UI is currently sending a message.
109     var isSendingMessage = false;
110     // A buffer of messages to send.
111     var sendMessageBuffer = [];
112     // These functions which will be generated when this module is
113     // first called. See generateRenderFunction for details.
114     var render = null;
115     // The list of renderers that have been registered to render
116     // this conversation. See generateRenderFunction for details.
117     var renderers = [];
119     var NEWEST_FIRST = Constants.NEWEST_MESSAGES_FIRST;
120     var LOAD_MESSAGE_LIMIT = Constants.LOAD_MESSAGE_LIMIT;
121     var MILLISECONDS_IN_SEC = Constants.MILLISECONDS_IN_SEC;
122     var SELECTORS = Constants.SELECTORS;
123     var CONVERSATION_TYPES = Constants.CONVERSATION_TYPES;
125     /**
126      * Get the other user userid.
127      *
128      * @return {Number} Userid.
129      */
130     var getOtherUserId = function() {
131         if (!viewState || viewState.type == CONVERSATION_TYPES.PUBLIC) {
132             return null;
133         }
135         var loggedInUserId = viewState.loggedInUserId;
136         if (viewState.type == CONVERSATION_TYPES.SELF) {
137             // It's a self-conversation, so the other user is the one logged in.
138             return loggedInUserId;
139         }
141         var otherUserIds = Object.keys(viewState.members).filter(function(userId) {
142             return loggedInUserId != userId;
143         });
145         return otherUserIds.length ? otherUserIds[0] : null;
146     };
148     /**
149      * Search the cache to see if we've already loaded a private conversation
150      * with the given user id.
151      *
152      * @param {Number} userId The id of the other user.
153      * @return {Number|null} Conversation id.
154      */
155     var getCachedPrivateConversationIdFromUserId = function(userId) {
156         return Object.keys(stateCache).reduce(function(carry, id) {
157             if (!carry) {
158                 var state = stateCache[id].state;
160                 if (state.type != CONVERSATION_TYPES.PUBLIC) {
161                     if (userId in state.members) {
162                         // We've found a cached conversation for this user!
163                         carry = state.id;
164                     }
165                 }
166             }
168             return carry;
169         }, null);
170     };
172     /**
173      * Get profile info for logged in user.
174      *
175      * @param {Object} body Conversation body container element.
176      * @return {Object}
177      */
178     var getLoggedInUserProfile = function(body) {
179         return {
180             id: parseInt(body.attr('data-user-id'), 10),
181             fullname: null,
182             profileimageurl: null,
183             profileimageurlsmall: null,
184             isonline:  null,
185             showonlinestatus: null,
186             isblocked: null,
187             iscontact: null,
188             isdeleted: null,
189             canmessage: null,
190             canmessageevenifblocked: null,
191             requirescontact: null,
192             contactrequests: []
193         };
194     };
196     /**
197      * Get the messages offset value to load more messages.
198      *
199      * @return {Number}
200      */
201     var getMessagesOffset = function() {
202         return messagesOffset;
203     };
205     /**
206      * Set the messages offset value for loading more messages.
207      *
208      * @param {Number} value The offset value
209      */
210     var setMessagesOffset = function(value) {
211         messagesOffset = value;
212         stateCache[viewState.id].messagesOffset = value;
213     };
215     /**
216      * Check if all messages have been loaded.
217      *
218      * @return {Bool}
219      */
220     var hasLoadedAllMessages = function() {
221         return loadedAllMessages;
222     };
224     /**
225      * Set whether all messages have been loaded or not.
226      *
227      * @param {Bool} value If all messages have been loaded.
228      */
229     var setLoadedAllMessages = function(value) {
230         loadedAllMessages = value;
231         stateCache[viewState.id].loadedAllMessages = value;
232     };
234     /**
235      * Get the messages container element.
236      *
237      * @param  {Object} body Conversation body container element.
238      * @return {Object} The messages container element.
239      */
240     var getMessagesContainer = function(body) {
241         return body.find(SELECTORS.MESSAGES_CONTAINER);
242     };
244     /**
245      * Reformat the conversation for an event payload.
246      *
247      * @param  {Object} state The view state.
248      * @return {Object} New formatted conversation.
249      */
250     var formatConversationForEvent = function(state) {
251         return {
252             id: state.id,
253             name: state.name,
254             subname: state.subname,
255             imageUrl: state.imageUrl,
256             isFavourite: state.isFavourite,
257             isMuted: state.isMuted,
258             type: state.type,
259             totalMemberCount: state.totalMemberCount,
260             loggedInUserId: state.loggedInUserId,
261             messages: state.messages.map(function(message) {
262                 return $.extend({}, message);
263             }),
264             members: Object.keys(state.members).map(function(id) {
265                 var formattedMember = $.extend({}, state.members[id]);
266                 formattedMember.contactrequests = state.members[id].contactrequests.map(function(request) {
267                     return $.extend({}, request);
268                 });
269                 return formattedMember;
270             })
271         };
272     };
274     /**
275      * Load up an empty private conversation between the logged in user and the
276      * other user. Sets all of the conversation details based on the other user.
277      *
278      * A conversation isn't created until the user sends the first message.
279      *
280      * @param  {Object} loggedInUserProfile The logged in user profile.
281      * @param  {Number} otherUserId The other user id.
282      * @return {Object} Profile returned from repository.
283      */
284     var loadEmptyPrivateConversation = function(loggedInUserProfile, otherUserId) {
285         var loggedInUserId = loggedInUserProfile.id;
286         // If the other user id is the same as the logged in user then this is a self
287         // conversation.
288         var conversationType = loggedInUserId == otherUserId ? CONVERSATION_TYPES.SELF : CONVERSATION_TYPES.PRIVATE;
289         var newState = StateManager.setLoadingMembers(viewState, true);
290         newState = StateManager.setLoadingMessages(newState, true);
291         render(newState);
293         return Repository.getMemberInfo(loggedInUserId, [otherUserId], true, true)
294             .then(function(profiles) {
295                 if (profiles.length) {
296                     return profiles[0];
297                 } else {
298                     throw new Error('Unable to load other user profile');
299                 }
300             })
301             .then(function(profile) {
302                 // If the conversation is a self conversation then the profile loaded is the
303                 // logged in user so only add that to the members array.
304                 var members = conversationType == CONVERSATION_TYPES.SELF ? [profile] : [profile, loggedInUserProfile];
305                 var newState = StateManager.addMembers(viewState, members);
306                 newState = StateManager.setLoadingMembers(newState, false);
307                 newState = StateManager.setLoadingMessages(newState, false);
308                 newState = StateManager.setName(newState, profile.fullname);
309                 newState = StateManager.setType(newState, conversationType);
310                 newState = StateManager.setImageUrl(newState, profile.profileimageurl);
311                 newState = StateManager.setTotalMemberCount(newState, members.length);
312                 render(newState);
313                 return profile;
314             })
315             .catch(function(error) {
316                 var newState = StateManager.setLoadingMembers(viewState, false);
317                 render(newState);
318                 Notification.exception(error);
319             });
320     };
322     /**
323      * Create a new state from a conversation object.
324      *
325      * @param {Object} conversation The conversation object.
326      * @param {Number} loggedInUserId The logged in user id.
327      * @return {Object} new state.
328      */
329     var updateStateFromConversation = function(conversation, loggedInUserId) {
330         var otherUser = null;
331         if (conversation.type == CONVERSATION_TYPES.PRIVATE) {
332             // For private conversations, remove current logged in user from the members list to get the other user.
333             var otherUsers = conversation.members.filter(function(member) {
334                 return member.id != loggedInUserId;
335             });
336             otherUser = otherUsers.length ? otherUsers[0] : null;
337         } else if (conversation.type == CONVERSATION_TYPES.SELF) {
338             // Self-conversations have only one member.
339             otherUser = conversation.members[0];
340         }
342         var name = conversation.name;
343         var imageUrl = conversation.imageurl;
345         if (conversation.type != CONVERSATION_TYPES.PUBLIC) {
346             name = name || otherUser ? otherUser.fullname : '';
347             imageUrl = imageUrl || otherUser ? otherUser.profileimageurl : '';
348         }
350         var newState = StateManager.addMembers(viewState, conversation.members);
351         newState = StateManager.setName(newState, name);
352         newState = StateManager.setSubname(newState, conversation.subname);
353         newState = StateManager.setType(newState, conversation.type);
354         newState = StateManager.setImageUrl(newState, imageUrl);
355         newState = StateManager.setTotalMemberCount(newState, conversation.membercount);
356         newState = StateManager.setIsFavourite(newState, conversation.isfavourite);
357         newState = StateManager.setIsMuted(newState, conversation.ismuted);
358         newState = StateManager.addMessages(newState, conversation.messages);
359         newState = StateManager.setCanDeleteMessagesForAllUsers(newState, conversation.candeletemessagesforallusers);
360         return newState;
361     };
363     /**
364      * Get the details for a conversation from the conversation id.
365      *
366      * @param  {Number} conversationId The conversation id.
367      * @param  {Object} loggedInUserProfile The logged in user profile.
368      * @param  {Number} messageLimit The number of messages to include.
369      * @param  {Number} messageOffset The number of messages to skip.
370      * @param  {Bool} newestFirst Order messages newest first.
371      * @return {Object} Promise resolved when loaded.
372      */
373     var loadNewConversation = function(
374         conversationId,
375         loggedInUserProfile,
376         messageLimit,
377         messageOffset,
378         newestFirst
379     ) {
380         var loggedInUserId = loggedInUserProfile.id;
381         var newState = StateManager.setLoadingMembers(viewState, true);
382         newState = StateManager.setLoadingMessages(newState, true);
383         render(newState);
385         return Repository.getConversation(
386             loggedInUserId,
387             conversationId,
388             true,
389             true,
390             0,
391             0,
392             messageLimit + 1,
393             messageOffset,
394             newestFirst
395         )
396             .then(function(conversation) {
397                 if (conversation.messages.length > messageLimit) {
398                     conversation.messages = conversation.messages.slice(1);
399                 } else {
400                     setLoadedAllMessages(true);
401                 }
403                 setMessagesOffset(messageOffset + messageLimit);
405                 return conversation;
406             })
407             .then(function(conversation) {
408                 var hasLoggedInUser = conversation.members.filter(function(member) {
409                     return member.id == loggedInUserProfile.id;
410                 });
412                 if (hasLoggedInUser.length < 1) {
413                     conversation.members = conversation.members.concat([loggedInUserProfile]);
414                 }
416                 var newState = updateStateFromConversation(conversation, loggedInUserProfile.id);
417                 newState = StateManager.setLoadingMembers(newState, false);
418                 newState = StateManager.setLoadingMessages(newState, false);
419                 return render(newState)
420                     .then(function() {
421                         return conversation;
422                     });
423             })
424             .then(function() {
425                 return markConversationAsRead(conversationId);
426             })
427             .catch(function(error) {
428                 var newState = StateManager.setLoadingMembers(viewState, false);
429                 newState = StateManager.setLoadingMessages(newState, false);
430                 render(newState);
431                 Notification.exception(error);
432             });
433     };
435     /**
436      * Get the details for a conversation from and existing conversation object.
437      *
438      * @param  {Object} conversation The conversation object.
439      * @param  {Object} loggedInUserProfile The logged in user profile.
440      * @param  {Number} messageLimit The number of messages to include.
441      * @param  {Bool} newestFirst Order messages newest first.
442      * @return {Object} Promise resolved when loaded.
443      */
444     var loadExistingConversation = function(
445         conversation,
446         loggedInUserProfile,
447         messageLimit,
448         newestFirst
449     ) {
450         var hasLoggedInUser = conversation.members.filter(function(member) {
451             return member.id == loggedInUserProfile.id;
452         });
454         if (hasLoggedInUser.length < 1) {
455             conversation.members = conversation.members.concat([loggedInUserProfile]);
456         }
458         var messageCount = conversation.messages.length;
459         var hasLoadedEnoughMessages = messageCount >= messageLimit;
460         var newState = updateStateFromConversation(conversation, loggedInUserProfile.id);
461         newState = StateManager.setLoadingMembers(newState, false);
462         newState = StateManager.setLoadingMessages(newState, !hasLoadedEnoughMessages);
463         var renderPromise = render(newState);
465         return renderPromise.then(function() {
466                 if (!hasLoadedEnoughMessages) {
467                     // We haven't got enough messages so let's load some more.
468                     return loadMessages(conversation.id, messageLimit, messageCount, newestFirst, []);
469                 } else {
470                     // We've got enough messages. No need to load any more for now.
471                     return {messages: conversation.messages};
472                 }
473             })
474             .then(function() {
475                 var messages = viewState.messages;
476                 // Update the offset to reflect the number of messages we've loaded.
477                 setMessagesOffset(messages.length);
478                 markConversationAsRead(viewState.id);
480                 return messages;
481             })
482             .catch(Notification.exception);
483     };
485     /**
486      * Load messages for this conversation and pass them to the renderer.
487      *
488      * @param  {Number} conversationId Conversation id.
489      * @param  {Number} limit Number of messages to load.
490      * @param  {Number} offset Get messages from offset.
491      * @param  {Bool} newestFirst Get newest messages first.
492      * @param  {Array} ignoreList Ignore any messages with ids in this list.
493      * @param  {Number|null} timeFrom Only get messages from this time onwards.
494      * @return {Promise} renderer promise.
495      */
496     var loadMessages = function(conversationId, limit, offset, newestFirst, ignoreList, timeFrom) {
497         return Repository.getMessages(
498                 viewState.loggedInUserId,
499                 conversationId,
500                 limit ? limit + 1 : limit,
501                 offset,
502                 newestFirst,
503                 timeFrom
504             )
505             .then(function(result) {
506                 if (result.messages.length && ignoreList.length) {
507                     result.messages = result.messages.filter(function(message) {
508                         // Skip any messages in our ignore list.
509                         return ignoreList.indexOf(parseInt(message.id, 10)) < 0;
510                     });
511                 }
513                 return result;
514             })
515             .then(function(result) {
516                 if (!limit) {
517                     return result;
518                 } else if (result.messages.length > limit) {
519                     // Ignore the last result which was just to test if there are more
520                     // to load.
521                     result.messages = result.messages.slice(0, -1);
522                 } else {
523                     setLoadedAllMessages(true);
524                 }
526                 return result;
527             })
528             .then(function(result) {
529                 var membersToAdd = result.members.filter(function(member) {
530                     return !(member.id in viewState.members);
531                 });
532                 var newState = StateManager.addMembers(viewState, membersToAdd);
533                 newState = StateManager.addMessages(newState, result.messages);
534                 newState = StateManager.setLoadingMessages(newState, false);
535                 return render(newState)
536                     .then(function() {
537                         return result;
538                     });
539             })
540             .catch(function(error) {
541                 var newState = StateManager.setLoadingMessages(viewState, false);
542                 render(newState);
543                 // Re-throw the error for other error handlers.
544                 throw error;
545             });
546     };
548     /**
549      * Create a callback function for getting new messages for this conversation.
550      *
551      * @param  {Number} conversationId Conversation id.
552      * @param  {Bool} newestFirst Show newest messages first
553      * @return {Function} Callback function that returns a renderer promise.
554      */
555     var getLoadNewMessagesCallback = function(conversationId, newestFirst) {
556         return function() {
557             var messages = viewState.messages;
558             var mostRecentMessage = messages.length ? messages[messages.length - 1] : null;
559             var lastTimeCreated = mostRecentMessage ? mostRecentMessage.timeCreated : null;
561             if (lastTimeCreated && !isResetting && !isSendingMessage) {
562                 // There may be multiple messages with the same time created value since
563                 // the accuracy is only down to the second. The server will include these
564                 // messages in the result (since it does a >= comparison on time from) so
565                 // we need to filter them back out of the result so that we're left only
566                 // with the new messages.
567                 var ignoreMessageIds = [];
568                 for (var i = messages.length - 1; i >= 0; i--) {
569                     var message = messages[i];
570                     if (message.timeCreated === lastTimeCreated) {
571                         ignoreMessageIds.push(message.id);
572                     } else {
573                         // Since the messages are ordered in ascending order of time created
574                         // we can break as soon as we hit a message with a different time created
575                         // because we know all other messages will have lower values.
576                         break;
577                     }
578                 }
580                 return loadMessages(
581                         conversationId,
582                         0,
583                         0,
584                         newestFirst,
585                         ignoreMessageIds,
586                         lastTimeCreated
587                     )
588                     .then(function(result) {
589                         if (result.messages.length) {
590                             // If we found some results then restart the polling timer
591                             // because the other user might be sending messages.
592                             newMessagesPollTimer.restart();
593                             // We've also got a new last message so publish that for other
594                             // components to update.
595                             var conversation = formatConversationForEvent(viewState);
596                             PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);
597                             return markConversationAsRead(conversationId);
598                         } else {
599                             return result;
600                         }
601                     });
602             }
604             return $.Deferred().resolve().promise();
605         };
606     };
608     /**
609      * Mark a conversation as read.
610      *
611      * @param  {Number} conversationId The conversation id.
612      * @return {Promise} The renderer promise.
613      */
614     var markConversationAsRead = function(conversationId) {
615         var loggedInUserId = viewState.loggedInUserId;
617         return Repository.markAllConversationMessagesAsRead(loggedInUserId, conversationId)
618             .then(function() {
619                 var newState = StateManager.markMessagesAsRead(viewState, viewState.messages);
620                 PubSub.publish(MessageDrawerEvents.CONVERSATION_READ, conversationId);
621                 return render(newState);
622             });
623     };
625     /**
626      * Tell the statemanager there is request to block a user and run the renderer
627      * to show the block user dialogue.
628      *
629      * @param {Number} userId User id.
630      */
631     var requestBlockUser = function(userId) {
632         cancelRequest(userId);
633         var newState = StateManager.addPendingBlockUsersById(viewState, [userId]);
634         render(newState);
635     };
637     /**
638      * Send the repository a request to block a user, update the statemanager and publish
639      * a contact has been blocked.
640      *
641      * @param  {Number} userId User id of user to block.
642      * @return {Promise} Renderer promise.
643      */
644     var blockUser = function(userId) {
645         var newState = StateManager.setLoadingConfirmAction(viewState, true);
646         render(newState);
648         return Repository.blockUser(viewState.loggedInUserId, userId)
649             .then(function(profile) {
650                 var newState = StateManager.addMembers(viewState, [profile]);
651                 newState = StateManager.removePendingBlockUsersById(newState, [userId]);
652                 newState = StateManager.setLoadingConfirmAction(newState, false);
653                 PubSub.publish(MessageDrawerEvents.CONTACT_BLOCKED, userId);
654                 return render(newState);
655             });
656     };
658     /**
659      * Tell the statemanager there is a request to unblock a user and run the renderer
660      * to show the unblock user dialogue.
661      *
662      * @param {Number} userId User id of user to unblock.
663      */
664     var requestUnblockUser = function(userId) {
665         cancelRequest(userId);
666         var newState = StateManager.addPendingUnblockUsersById(viewState, [userId]);
667         render(newState);
668     };
670     /**
671      * Send the repository a request to unblock a user, update the statemanager and publish
672      * a contact has been unblocked.
673      *
674      * @param  {Number} userId User id of user to unblock.
675      * @return {Promise} Renderer promise.
676      */
677     var unblockUser = function(userId) {
678         var newState = StateManager.setLoadingConfirmAction(viewState, true);
679         render(newState);
681         return Repository.unblockUser(viewState.loggedInUserId, userId)
682             .then(function(profile) {
683                 var newState = StateManager.addMembers(viewState, [profile]);
684                 newState = StateManager.removePendingUnblockUsersById(newState, [userId]);
685                 newState = StateManager.setLoadingConfirmAction(newState, false);
686                 PubSub.publish(MessageDrawerEvents.CONTACT_UNBLOCKED, userId);
687                 return render(newState);
688             });
689     };
691     /**
692      * Tell the statemanager there is a request to remove a user from the contact list
693      * and run the renderer to show the remove user from contacts dialogue.
694      *
695      * @param {Number} userId User id of user to remove from contacts.
696      */
697     var requestRemoveContact = function(userId) {
698         cancelRequest(userId);
699         var newState = StateManager.addPendingRemoveContactsById(viewState, [userId]);
700         render(newState);
701     };
703     /**
704      * Send the repository a request to remove a user from the contacts list. update the statemanager
705      * and publish a contact has been removed.
706      *
707      * @param  {Number} userId User id of user to remove from contacts.
708      * @return {Promise} Renderer promise.
709      */
710     var removeContact = function(userId) {
711         var newState = StateManager.setLoadingConfirmAction(viewState, true);
712         render(newState);
714         return Repository.deleteContacts(viewState.loggedInUserId, [userId])
715             .then(function(profiles) {
716                 var newState = StateManager.addMembers(viewState, profiles);
717                 newState = StateManager.removePendingRemoveContactsById(newState, [userId]);
718                 newState = StateManager.setLoadingConfirmAction(newState, false);
719                 PubSub.publish(MessageDrawerEvents.CONTACT_REMOVED, userId);
720                 return render(newState);
721             });
722     };
724     /**
725      * Tell the statemanager there is a request to add a user to the contact list
726      * and run the renderer to show the add user to contacts dialogue.
727      *
728      * @param {Number} userId User id of user to add to contacts.
729      */
730     var requestAddContact = function(userId) {
731         cancelRequest(userId);
732         var newState = StateManager.addPendingAddContactsById(viewState, [userId]);
733         render(newState);
734     };
736     /**
737      * Send the repository a request to add a user to the contacts list. update the statemanager
738      * and publish a contact has been added.
739      *
740      * @param  {Number} userId User id of user to add to contacts.
741      * @return {Promise} Renderer promise.
742      */
743     var addContact = function(userId) {
744         var newState = StateManager.setLoadingConfirmAction(viewState, true);
745         render(newState);
747         return Repository.createContactRequest(viewState.loggedInUserId, userId)
748             .then(function(response) {
749                 if (!response.request) {
750                     throw new Error(response.warnings[0].message);
751                 }
753                 return response.request;
754             })
755             .then(function(request) {
756                 var newState = StateManager.removePendingAddContactsById(viewState, [userId]);
757                 newState = StateManager.addContactRequests(newState, [request]);
758                 newState = StateManager.setLoadingConfirmAction(newState, false);
759                 return render(newState);
760             });
761     };
763     /**
764      * Set the current conversation as a favourite conversation.
765      *
766      * @return {Promise} Renderer promise.
767      */
768     var setFavourite = function() {
769         var userId = viewState.loggedInUserId;
770         var conversationId = viewState.id;
772         return Repository.setFavouriteConversations(userId, [conversationId])
773             .then(function() {
774                 var newState = StateManager.setIsFavourite(viewState, true);
775                 return render(newState);
776             })
777             .then(function() {
778                 return PubSub.publish(
779                     MessageDrawerEvents.CONVERSATION_SET_FAVOURITE,
780                     formatConversationForEvent(viewState)
781                 );
782             });
783     };
785     /**
786      * Unset the current conversation as a favourite conversation.
787      *
788      * @return {Promise} Renderer promise.
789      */
790     var unsetFavourite = function() {
791         var userId = viewState.loggedInUserId;
792         var conversationId = viewState.id;
794         return Repository.unsetFavouriteConversations(userId, [conversationId])
795             .then(function() {
796                 var newState = StateManager.setIsFavourite(viewState, false);
797                 return render(newState);
798             })
799             .then(function() {
800                 return PubSub.publish(
801                     MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE,
802                     formatConversationForEvent(viewState)
803                 );
804             });
805     };
807     /**
808      * Set the current conversation as a muted conversation.
809      *
810      * @return {Promise} Renderer promise.
811      */
812     var setMuted = function() {
813         var userId = viewState.loggedInUserId;
814         var conversationId = viewState.id;
816         return Repository.setMutedConversations(userId, [conversationId])
817             .then(function() {
818                 var newState = StateManager.setIsMuted(viewState, true);
819                 return render(newState);
820             })
821             .then(function() {
822                 return PubSub.publish(
823                     MessageDrawerEvents.CONVERSATION_SET_MUTED,
824                     formatConversationForEvent(viewState)
825                 );
826             });
827     };
829     /**
830      * Unset the current conversation as a muted conversation.
831      *
832      * @return {Promise} Renderer promise.
833      */
834     var unsetMuted = function() {
835         var userId = viewState.loggedInUserId;
836         var conversationId = viewState.id;
838         return Repository.unsetMutedConversations(userId, [conversationId])
839             .then(function() {
840                 var newState = StateManager.setIsMuted(viewState, false);
841                 return render(newState);
842             })
843             .then(function() {
844                 return PubSub.publish(
845                     MessageDrawerEvents.CONVERSATION_UNSET_MUTED,
846                     formatConversationForEvent(viewState)
847                 );
848             });
849     };
851     /**
852      * Tell the statemanager there is a request to delete the selected messages
853      * and run the renderer to show confirm delete messages dialogue.
854      *
855      * @param {Number} userId User id.
856      */
857     var requestDeleteSelectedMessages = function(userId) {
858         var selectedMessageIds = viewState.selectedMessageIds;
859         cancelRequest(userId);
860         var newState = StateManager.addPendingDeleteMessagesById(viewState, selectedMessageIds);
861         render(newState);
862     };
864     /**
865      * Send the repository a request to delete the messages pending deletion. Update the statemanager
866      * and publish a message deletion event.
867      *
868      * @return {Promise} Renderer promise.
869      */
870     var deleteSelectedMessages = function() {
871         var messageIds = viewState.pendingDeleteMessageIds;
872         var sentMessages = viewState.messages.filter(function(message) {
873             // If a message sendState is null then it means it was loaded from the server or if it's
874             // set to sent then it means the user has successfully sent it in this page load.
875             return messageIds.indexOf(message.id) >= 0 && (message.sendState == 'sent' || message.sendState === null);
876         });
877         var newState = StateManager.setLoadingConfirmAction(viewState, true);
879         render(newState);
881         var deleteMessagesPromise = $.Deferred().resolve().promise();
883         if (sentMessages.length) {
884             // We only need to send a request to the server if we're trying to delete messages that
885             // have successfully been sent.
886             var sentMessageIds = sentMessages.map(function(message) {
887                 return message.id;
888             });
889             if (newState.deleteMessagesForAllUsers) {
890                 deleteMessagesPromise = Repository.deleteMessagesForAllUsers(viewState.loggedInUserId, sentMessageIds);
891             } else {
892                 deleteMessagesPromise = Repository.deleteMessages(viewState.loggedInUserId, sentMessageIds);
893             }
894         }
896         return deleteMessagesPromise.then(function() {
897                 var newState = StateManager.removeMessagesById(viewState, messageIds);
898                 newState = StateManager.removePendingDeleteMessagesById(newState, messageIds);
899                 newState = StateManager.removeSelectedMessagesById(newState, messageIds);
900                 newState = StateManager.setLoadingConfirmAction(newState, false);
901                 newState = StateManager.setDeleteMessagesForAllUsers(newState, false);
903                 var prevLastMessage = viewState.messages[viewState.messages.length - 1];
904                 var newLastMessage = newState.messages.length ? newState.messages[newState.messages.length - 1] : null;
906                 if (newLastMessage && newLastMessage.id != prevLastMessage.id) {
907                     var conversation = formatConversationForEvent(newState);
908                     PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);
909                 } else if (!newState.messages.length) {
910                     PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id);
911                 }
913                 return render(newState);
914             })
915             .catch(Notification.exception);
916     };
918     /**
919      * Tell the statemanager there is a request to delete a conversation
920      * and run the renderer to show confirm delete conversation dialogue.
921      *
922      * @param {Number} userId User id of other user.
923      */
924     var requestDeleteConversation = function(userId) {
925         cancelRequest(userId);
926         var newState = StateManager.setPendingDeleteConversation(viewState, true);
927         render(newState);
928     };
930     /**
931      * Send the repository a request to delete a conversation. Update the statemanager
932      * and publish a conversation deleted event.
933      *
934      * @return {Promise} Renderer promise.
935      */
936     var deleteConversation = function() {
937         var newState = StateManager.setLoadingConfirmAction(viewState, true);
938         render(newState);
940         return Repository.deleteConversation(viewState.loggedInUserId, viewState.id)
941             .then(function() {
942                 var newState = StateManager.removeMessages(viewState, viewState.messages);
943                 newState = StateManager.removeSelectedMessagesById(newState, viewState.selectedMessageIds);
944                 newState = StateManager.setPendingDeleteConversation(newState, false);
945                 newState = StateManager.setLoadingConfirmAction(newState, false);
946                 PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id);
948                 return render(newState);
949             });
950     };
952     /**
953      * Tell the statemanager to cancel all pending actions.
954      *
955      * @param  {Number} userId User id.
956      */
957     var cancelRequest = function(userId) {
958         var pendingDeleteMessageIds = viewState.pendingDeleteMessageIds;
959         var newState = StateManager.removePendingAddContactsById(viewState, [userId]);
960         newState = StateManager.removePendingRemoveContactsById(newState, [userId]);
961         newState = StateManager.removePendingUnblockUsersById(newState, [userId]);
962         newState = StateManager.removePendingBlockUsersById(newState, [userId]);
963         newState = StateManager.removePendingDeleteMessagesById(newState, pendingDeleteMessageIds);
964         newState = StateManager.setPendingDeleteConversation(newState, false);
965         newState = StateManager.setDeleteMessagesForAllUsers(newState, false);
966         render(newState);
967     };
969     /**
970      * Accept the contact request from the given user.
971      *
972      * @param  {Number} userId User id of other user.
973      * @return {Promise} Renderer promise.
974      */
975     var acceptContactRequest = function(userId) {
976         // Search the list of the logged in user's contact requests to find the
977         // one from this user.
978         var loggedInUserId = viewState.loggedInUserId;
979         var requests = viewState.members[userId].contactrequests.filter(function(request) {
980             return request.requesteduserid == loggedInUserId;
981         });
982         var request = requests[0];
983         var newState = StateManager.setLoadingConfirmAction(viewState, true);
984         render(newState);
986         return Repository.acceptContactRequest(userId, loggedInUserId)
987             .then(function(profile) {
988                 var newState = StateManager.removeContactRequests(viewState, [request]);
989                 newState = StateManager.addMembers(viewState, [profile]);
990                 newState = StateManager.setLoadingConfirmAction(newState, false);
991                 return render(newState);
992             })
993             .then(function() {
994                 PubSub.publish(MessageDrawerEvents.CONTACT_ADDED, viewState.members[userId]);
995                 PubSub.publish(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, request);
996                 return;
997             });
998     };
1000     /**
1001      * Decline the contact request from the given user.
1002      *
1003      * @param  {Number} userId User id of other user.
1004      * @return {Promise} Renderer promise.
1005      */
1006     var declineContactRequest = function(userId) {
1007         // Search the list of the logged in user's contact requests to find the
1008         // one from this user.
1009         var loggedInUserId = viewState.loggedInUserId;
1010         var requests = viewState.members[userId].contactrequests.filter(function(request) {
1011             return request.requesteduserid == loggedInUserId;
1012         });
1013         var request = requests[0];
1014         var newState = StateManager.setLoadingConfirmAction(viewState, true);
1015         render(newState);
1017         return Repository.declineContactRequest(userId, loggedInUserId)
1018             .then(function(profile) {
1019                 var newState = StateManager.removeContactRequests(viewState, [request]);
1020                 newState = StateManager.addMembers(viewState, [profile]);
1021                 newState = StateManager.setLoadingConfirmAction(newState, false);
1022                 return render(newState);
1023             })
1024             .then(function() {
1025                 PubSub.publish(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, request);
1026                 return;
1027             });
1028     };
1030     /**
1031      * Send all of the messages in the buffer to the server to be created. Update the
1032      * UI with the newly created message information.
1033      *
1034      * This function will recursively call itself in order to make sure the buffer is
1035      * always being processed.
1036      */
1037     var processSendMessageBuffer = function() {
1038         if (isSendingMessage) {
1039             // We're already sending messages so nothing to do.
1040             return;
1041         }
1042         if (!sendMessageBuffer.length) {
1043             // No messages waiting to send. Nothing to do.
1044             return;
1045         }
1047         // Flag that we're processing the queue.
1048         isSendingMessage = true;
1049         // Grab all of the messages in the buffer.
1050         var messagesToSend = sendMessageBuffer.slice();
1051         // Empty the buffer since we're processing it.
1052         sendMessageBuffer = [];
1053         var conversationId = viewState.id;
1054         var newConversationId = null;
1055         var messagesText = messagesToSend.map(function(message) {
1056             return message.text;
1057         });
1058         var messageIds = messagesToSend.map(function(message) {
1059             return message.id;
1060         });
1061         var sendMessagePromise = null;
1062         var newCanDeleteMessagesForAllUsers = null;
1063         if (!conversationId && (viewState.type != CONVERSATION_TYPES.PUBLIC)) {
1064             // If it's a new private conversation then we need to use the old
1065             // web service function to create the conversation.
1066             var otherUserId = getOtherUserId();
1067             sendMessagePromise = Repository.sendMessagesToUser(otherUserId, messagesText)
1068                 .then(function(messages) {
1069                     if (messages.length) {
1070                         newConversationId = parseInt(messages[0].conversationid, 10);
1071                         newCanDeleteMessagesForAllUsers = messages[0].candeletemessagesforallusers;
1072                     }
1073                     return messages;
1074                 });
1075         } else {
1076             sendMessagePromise = Repository.sendMessagesToConversation(conversationId, messagesText);
1077         }
1079         sendMessagePromise
1080             .then(function(messages) {
1081                 var newMessageIds = messages.map(function(message) {
1082                     return message.id;
1083                 });
1084                 var data = [];
1085                 var selectedToRemove = [];
1086                 var selectedToAdd = [];
1088                 messagesToSend.forEach(function(oldMessage, index) {
1089                     var newMessage = messages[index];
1090                     // Update messages expects and array of arrays where the first value
1091                     // is the old message to update and the second value is the new values
1092                     // to set.
1093                     data.push([oldMessage, newMessage]);
1095                     if (viewState.selectedMessageIds.indexOf(oldMessage.id) >= 0) {
1096                         // If the message was added to the "selected messages" list while it was still
1097                         // being sent then we should update it's id in that list now to make sure future
1098                         // actions work.
1099                         selectedToRemove.push(oldMessage.id);
1100                         selectedToAdd.push(newMessage.id);
1101                     }
1102                 });
1103                 var newState = StateManager.updateMessages(viewState, data);
1104                 newState = StateManager.setMessagesSendSuccessById(newState, newMessageIds);
1106                 if (selectedToRemove.length) {
1107                     newState = StateManager.removeSelectedMessagesById(newState, selectedToRemove);
1108                 }
1110                 if (selectedToAdd.length) {
1111                     newState = StateManager.addSelectedMessagesById(newState, selectedToAdd);
1112                 }
1114                 var conversation = formatConversationForEvent(newState);
1116                 if (!newState.id) {
1117                     // If this message created the conversation then save the conversation
1118                     // id.
1119                     newState = StateManager.setId(newState, newConversationId);
1120                     conversation.id = newConversationId;
1121                     resetMessagePollTimer(newConversationId);
1122                     PubSub.publish(MessageDrawerEvents.CONVERSATION_CREATED, conversation);
1123                     newState = StateManager.setCanDeleteMessagesForAllUsers(newState, newCanDeleteMessagesForAllUsers);
1124                 }
1126                 // Update the UI with the new message values from the server.
1127                 render(newState);
1128                 // Recurse just in case there has been more messages added to the buffer.
1129                 isSendingMessage = false;
1130                 processSendMessageBuffer();
1131                 PubSub.publish(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, conversation);
1132                 return;
1133             })
1134             .catch(function(e) {
1135                 if (e.message) {
1136                     var errorMessage =  $.Deferred().resolve(e.message).promise();
1137                 } else {
1138                     var errorMessage =  Str.get_string('unknownerror', 'core');
1139                 }
1141                 var handleFailedMessages = function(errorMessage) {
1142                     // We failed to create messages so remove the old messages from the pending queue
1143                     // and update the UI to indicate that the message failed.
1144                     var newState = StateManager.setMessagesSendFailById(viewState, messageIds, errorMessage);
1145                     render(newState);
1146                     isSendingMessage = false;
1147                     processSendMessageBuffer();
1148                 };
1150                 errorMessage.then(handleFailedMessages)
1151                     .catch(function(e) {
1152                         // Hrmm, we can't even load the error messages string! We'll have to
1153                         // hard code something in English here if we still haven't got a message
1154                         // to show.
1155                         var finalError = e.message || 'Something went wrong!';
1156                         handleFailedMessages(finalError);
1157                     });
1158             });
1159     };
1161     /**
1162      * Buffers messages to be sent to the server. We use a buffer here to allow the
1163      * user to freely input messages without blocking the interface for them.
1164      *
1165      * Instead we just queue all of their messages up and send them as fast as we can.
1166      *
1167      * @param {String} text Text to send.
1168      */
1169     var sendMessage = function(text) {
1170         var id = 'temp' + Date.now();
1171         var message = {
1172             id: id,
1173             useridfrom: viewState.loggedInUserId,
1174             text: text,
1175             timecreated: null
1176         };
1177         var newState = StateManager.addMessages(viewState, [message]);
1178         render(newState);
1179         sendMessageBuffer.push(message);
1180         processSendMessageBuffer();
1181     };
1183     /**
1184      * Retry sending a message that failed.
1185      *
1186      * @param {Object} message The message to send.
1187      */
1188     var retrySendMessage = function(message) {
1189         var newState = StateManager.setMessagesSendPendingById(viewState, [message.id]);
1190         render(newState);
1191         sendMessageBuffer.push(message);
1192         processSendMessageBuffer();
1193     };
1195     /**
1196      * Toggle the selected messages update the statemanager and render the result.
1197      *
1198      * @param  {Number} messageId The id of the message to be toggled
1199      */
1200     var toggleSelectMessage = function(messageId) {
1201         var newState = viewState;
1203         if (viewState.selectedMessageIds.indexOf(messageId) > -1) {
1204             newState = StateManager.removeSelectedMessagesById(viewState, [messageId]);
1205         } else {
1206             newState = StateManager.addSelectedMessagesById(viewState, [messageId]);
1207         }
1209         render(newState);
1210     };
1212     /**
1213      * Cancel edit mode (selecting the messages).
1214      *
1215      * @return {Promise} Renderer promise.
1216      */
1217     var cancelEditMode = function() {
1218         cancelRequest(getOtherUserId());
1219         var newState = StateManager.removeSelectedMessagesById(viewState, viewState.selectedMessageIds);
1220         render(newState);
1221     };
1223     /**
1224      * Process the patches in the render buffer one at a time in order until the
1225      * buffer is empty.
1226      *
1227      * @param {Object} header The conversation header container element.
1228      * @param {Object} body The conversation body container element.
1229      * @param {Object} footer The conversation footer container element.
1230      */
1231     var processRenderBuffer = function(header, body, footer) {
1232         if (isRendering) {
1233             return;
1234         }
1236         if (!renderBuffer.length) {
1237             return;
1238         }
1240         isRendering = true;
1241         var renderable = renderBuffer.shift();
1242         var renderPromises = renderers.map(function(renderFunc) {
1243             return renderFunc(renderable.patch);
1244         });
1246         $.when.apply(null, renderPromises)
1247             .then(function() {
1248                 isRendering = false;
1249                 renderable.deferred.resolve(true);
1250                 // Keep processing the buffer until it's empty.
1251                 processRenderBuffer(header, body, footer);
1252             })
1253             .catch(function(error) {
1254                 isRendering = false;
1255                 renderable.deferred.reject(error);
1256                 Notification.exception(error);
1257             });
1258     };
1260     /**
1261      * Create a function to render the Conversation.
1262      *
1263      * @param  {Object} header The conversation header container element.
1264      * @param  {Object} body The conversation body container element.
1265      * @param  {Object} footer The conversation footer container element.
1266      * @param  {Bool} isNewConversation Has someone else already initialised a conversation?
1267      * @return {Promise} Renderer promise.
1268      */
1269     var generateRenderFunction = function(header, body, footer, isNewConversation) {
1270         var rendererFunc = function(patch) {
1271             return Renderer.render(header, body, footer, patch);
1272         };
1274         if (!isNewConversation) {
1275             // Looks like someone got here before us! We'd better update our
1276             // UI to make sure it matches.
1277             var initialState = StateManager.buildInitialState(viewState.midnight, viewState.loggedInUserId, viewState.id);
1278             var syncPatch = Patcher.buildPatch(initialState, viewState);
1279             rendererFunc(syncPatch);
1280         }
1282         renderers.push(rendererFunc);
1284         return function(newState) {
1285             var patch = Patcher.buildPatch(viewState, newState);
1286             var deferred = $.Deferred();
1288             // Check if the patch has any data. Ignore empty patches.
1289             if (Object.keys(patch).length) {
1290                 // Add the patch to the render buffer which gets processed in order.
1291                 renderBuffer.push({
1292                     patch: patch,
1293                     deferred: deferred
1294                 });
1295             } else {
1296                 deferred.resolve(true);
1297             }
1298             // This is a great place to add in some console logging if you need
1299             // to debug something. You can log the current state, the next state,
1300             // and the generated patch and see exactly what will be updated.
1302             // Optimistically update the state. We're going to assume that the rendering
1303             // will always succeed. The rendering is asynchronous (annoyingly) so it's buffered
1304             // but it'll reach eventual consistency with the current state.
1305             viewState = newState;
1306             if (newState.id) {
1307                 // Only cache created conversations.
1308                 stateCache[newState.id] = {
1309                     state: newState,
1310                     messagesOffset: getMessagesOffset(),
1311                     loadedAllMessages: hasLoadedAllMessages()
1312                 };
1313             }
1315             // Start processing the buffer.
1316             processRenderBuffer(header, body, footer);
1318             return deferred.promise();
1319         };
1320     };
1322     /**
1323      * Create a confirm action function.
1324      *
1325      * @param {Function} actionCallback The callback function.
1326      * @return {Function} Confirm action handler.
1327      */
1328     var generateConfirmActionHandler = function(actionCallback) {
1329         return function(e, data) {
1330             if (!viewState.loadingConfirmAction) {
1331                 actionCallback(getOtherUserId());
1332                 var newState = StateManager.setLoadingConfirmAction(viewState, false);
1333                 render(newState);
1334             }
1335             data.originalEvent.preventDefault();
1336         };
1337     };
1339     /**
1340      * Send message event handler.
1341      *
1342      * @param {Object} e Element this event handler is called on.
1343      * @param {Object} data Data for this event.
1344      */
1345     var handleSendMessage = function(e, data) {
1346         var target = $(e.target);
1347         var footerContainer = target.closest(SELECTORS.FOOTER_CONTAINER);
1348         var textArea = footerContainer.find(SELECTORS.MESSAGE_TEXT_AREA);
1349         var text = textArea.val().trim();
1351         if (text !== '') {
1352             sendMessage(text);
1353             textArea.val('');
1354             textArea.focus();
1355         }
1357         data.originalEvent.preventDefault();
1358     };
1360     /**
1361      * Select message event handler.
1362      *
1363      * @param {Object} e Element this event handler is called on.
1364      * @param {Object} data Data for this event.
1365      */
1366     var handleSelectMessage = function(e, data) {
1367         var selection = window.getSelection();
1368         var target = $(e.target);
1370         if (selection.toString() != '') {
1371             // Bail if we're selecting.
1372             return;
1373         }
1375         if (target.is('a')) {
1376             // Clicking on a link in the message so ignore it.
1377             return;
1378         }
1380         var element = target.closest(SELECTORS.MESSAGE);
1381         var messageId = element.attr('data-message-id');
1383         toggleSelectMessage(messageId);
1385         data.originalEvent.preventDefault();
1386     };
1388     /**
1389      * Handle retry sending of message.
1390      *
1391      * @param {Object} e Element this event handler is called on.
1392      * @param {Object} data Data for this event.
1393      */
1394     var handleRetrySendMessage = function(e, data) {
1395         var target = $(e.target);
1396         var element = target.closest(SELECTORS.MESSAGE);
1397         var messageId = element.attr('data-message-id');
1398         var messages = viewState.messages.filter(function(message) {
1399             return message.id == messageId;
1400         });
1401         var message = messages.length ? messages[0] : null;
1403         if (message) {
1404             retrySendMessage(message);
1405         }
1407         data.originalEvent.preventDefault();
1408         data.originalEvent.stopPropagation();
1409         e.stopPropagation();
1410     };
1412     /**
1413      * Cancel edit mode event handler.
1414      *
1415      * @param {Object} e Element this event handler is called on.
1416      * @param {Object} data Data for this event.
1417      */
1418     var handleCancelEditMode = function(e, data) {
1419         cancelEditMode();
1420         data.originalEvent.preventDefault();
1421     };
1423     /**
1424      * Show the view contact page.
1425      *
1426      * @param {String} namespace Unique identifier for the Routes
1427      * @return {Function} View contact handler.
1428      */
1429     var generateHandleViewContact = function(namespace) {
1430         return function(e, data) {
1431             var otherUserId = getOtherUserId();
1432             var otherUser = viewState.members[otherUserId];
1433             MessageDrawerRouter.go(namespace, MessageDrawerRoutes.VIEW_CONTACT, otherUser);
1434             data.originalEvent.preventDefault();
1435         };
1436     };
1438     /**
1439      * Set this conversation as a favourite.
1440      *
1441      * @param {Object} e Element this event handler is called on.
1442      * @param {Object} data Data for this event.
1443      */
1444     var handleSetFavourite = function(e, data) {
1445         setFavourite().catch(Notification.exception);
1446         data.originalEvent.preventDefault();
1447     };
1449     /**
1450      * Unset this conversation as a favourite.
1451      *
1452      * @param {Object} e Element this event handler is called on.
1453      * @param {Object} data Data for this event.
1454      */
1455     var handleUnsetFavourite = function(e, data) {
1456         unsetFavourite().catch(Notification.exception);
1457         data.originalEvent.preventDefault();
1458     };
1460     /**
1461      * Show the view group info page.
1462      * Set this conversation as muted.
1463      *
1464      * @param {Object} e Element this event handler is called on.
1465      * @param {Object} data Data for this event.
1466      */
1467     var handleSetMuted = function(e, data) {
1468         setMuted().catch(Notification.exception);
1469         data.originalEvent.preventDefault();
1470     };
1472     /**
1473      * Unset this conversation as muted.
1474      *
1475      * @param {Object} e Element this event handler is called on.
1476      * @param {Object} data Data for this event.
1477      */
1478     var handleUnsetMuted = function(e, data) {
1479         unsetMuted().catch(Notification.exception);
1480         data.originalEvent.preventDefault();
1481     };
1483     /**
1484      * Handle clicking on the checkbox that toggles deleting messages for
1485      * all users.
1486      *
1487      * @param {Object} e Element this event handler is called on.
1488      */
1489     var handleDeleteMessagesForAllUsersToggle = function(e) {
1490         var newValue = $(e.target).prop('checked');
1491         var newState = StateManager.setDeleteMessagesForAllUsers(viewState, newValue);
1492         render(newState);
1493     };
1495     /**
1496      * Show the view contact page.
1497      *
1498      * @param {String} namespace Unique identifier for the Routes
1499      * @return {Function} View group info handler.
1500      */
1501     var generateHandleViewGroupInfo = function(namespace) {
1502         return function(e, data) {
1503             MessageDrawerRouter.go(
1504                 namespace,
1505                 MessageDrawerRoutes.VIEW_GROUP_INFO,
1506                 {
1507                     id: viewState.id,
1508                     name: viewState.name,
1509                     subname: viewState.subname,
1510                     imageUrl: viewState.imageUrl,
1511                     totalMemberCount: viewState.totalMemberCount
1512                 },
1513                 viewState.loggedInUserId
1514             );
1515             data.originalEvent.preventDefault();
1516         };
1517     };
1519     /**
1520      * Handle clicking on the emoji toggle button.
1521      *
1522      * @param {Object} e The event
1523      * @param {Object} data The custom interaction event data
1524      */
1525     var handleToggleEmojiPicker = function(e, data) {
1526         var newState = StateManager.setShowEmojiPicker(viewState, !viewState.showEmojiPicker);
1527         render(newState);
1528         data.originalEvent.preventDefault();
1529     };
1531     /**
1532      * Handle clicking outside the emoji picker to close it.
1533      *
1534      * @param {Object} e The event
1535      */
1536     var handleCloseEmojiPicker = function(e) {
1537         var target = $(e.target);
1539         if (
1540             viewState.showEmojiPicker &&
1541             !target.closest(SELECTORS.EMOJI_PICKER_CONTAINER).length &&
1542             !target.closest(SELECTORS.TOGGLE_EMOJI_PICKER_BUTTON).length
1543         ) {
1544             var newState = StateManager.setShowEmojiPicker(viewState, false);
1545             render(newState);
1546         }
1547     };
1549     /**
1550      * Listen to, and handle events for conversations.
1551      *
1552      * @param {string} namespace The route namespace.
1553      * @param {Object} header Conversation header container element.
1554      * @param {Object} body Conversation body container element.
1555      * @param {Object} footer Conversation footer container element.
1556      */
1557     var registerEventListeners = function(namespace, header, body, footer) {
1558         var isLoadingMoreMessages = false;
1559         var messagesContainer = getMessagesContainer(body);
1560         var emojiPickerElement = footer.find(SELECTORS.EMOJI_PICKER);
1561         var emojiAutoCompleteContainer = footer.find(SELECTORS.EMOJI_AUTO_COMPLETE_CONTAINER);
1562         var messageTextArea = footer.find(SELECTORS.MESSAGE_TEXT_AREA);
1563         var headerActivateHandlers = [
1564             [SELECTORS.ACTION_REQUEST_BLOCK, generateConfirmActionHandler(requestBlockUser)],
1565             [SELECTORS.ACTION_REQUEST_UNBLOCK, generateConfirmActionHandler(requestUnblockUser)],
1566             [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)],
1567             [SELECTORS.ACTION_REQUEST_REMOVE_CONTACT, generateConfirmActionHandler(requestRemoveContact)],
1568             [SELECTORS.ACTION_REQUEST_DELETE_CONVERSATION, generateConfirmActionHandler(requestDeleteConversation)],
1569             [SELECTORS.ACTION_CANCEL_EDIT_MODE, handleCancelEditMode],
1570             [SELECTORS.ACTION_VIEW_CONTACT, generateHandleViewContact(namespace)],
1571             [SELECTORS.ACTION_VIEW_GROUP_INFO, generateHandleViewGroupInfo(namespace)],
1572             [SELECTORS.ACTION_CONFIRM_FAVOURITE, handleSetFavourite],
1573             [SELECTORS.ACTION_CONFIRM_MUTE, handleSetMuted],
1574             [SELECTORS.ACTION_CONFIRM_UNFAVOURITE, handleUnsetFavourite],
1575             [SELECTORS.ACTION_CONFIRM_UNMUTE, handleUnsetMuted]
1576         ];
1577         var bodyActivateHandlers = [
1578             [SELECTORS.ACTION_CANCEL_CONFIRM, generateConfirmActionHandler(cancelRequest)],
1579             [SELECTORS.ACTION_CONFIRM_BLOCK, generateConfirmActionHandler(blockUser)],
1580             [SELECTORS.ACTION_CONFIRM_UNBLOCK, generateConfirmActionHandler(unblockUser)],
1581             [SELECTORS.ACTION_CONFIRM_ADD_CONTACT, generateConfirmActionHandler(addContact)],
1582             [SELECTORS.ACTION_CONFIRM_REMOVE_CONTACT, generateConfirmActionHandler(removeContact)],
1583             [SELECTORS.ACTION_CONFIRM_DELETE_SELECTED_MESSAGES, generateConfirmActionHandler(deleteSelectedMessages)],
1584             [SELECTORS.ACTION_CONFIRM_DELETE_CONVERSATION, generateConfirmActionHandler(deleteConversation)],
1585             [SELECTORS.ACTION_OKAY_CONFIRM, generateConfirmActionHandler(cancelRequest)],
1586             [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)],
1587             [SELECTORS.ACTION_ACCEPT_CONTACT_REQUEST, generateConfirmActionHandler(acceptContactRequest)],
1588             [SELECTORS.ACTION_DECLINE_CONTACT_REQUEST, generateConfirmActionHandler(declineContactRequest)],
1589             [SELECTORS.MESSAGE, handleSelectMessage],
1590             [SELECTORS.DELETE_MESSAGES_FOR_ALL_USERS_TOGGLE, handleDeleteMessagesForAllUsersToggle],
1591             [SELECTORS.RETRY_SEND, handleRetrySendMessage]
1592         ];
1593         var footerActivateHandlers = [
1594             [SELECTORS.SEND_MESSAGE_BUTTON, handleSendMessage],
1595             [SELECTORS.TOGGLE_EMOJI_PICKER_BUTTON, handleToggleEmojiPicker],
1596             [SELECTORS.ACTION_REQUEST_DELETE_SELECTED_MESSAGES, generateConfirmActionHandler(requestDeleteSelectedMessages)],
1597             [SELECTORS.ACTION_REQUEST_ADD_CONTACT, generateConfirmActionHandler(requestAddContact)],
1598             [SELECTORS.ACTION_REQUEST_UNBLOCK, generateConfirmActionHandler(requestUnblockUser)],
1599         ];
1601         AutoRows.init(footer);
1603         if (emojiAutoCompleteContainer.length) {
1604             initialiseEmojiAutoComplete(
1605                 emojiAutoCompleteContainer[0],
1606                 messageTextArea[0],
1607                 function(hasSuggestions) {
1608                     var newState = StateManager.setShowEmojiAutoComplete(viewState, hasSuggestions);
1609                     render(newState);
1610                 },
1611                 function(emoji) {
1612                     var newState = StateManager.setShowEmojiAutoComplete(viewState, false);
1613                     render(newState);
1615                     messageTextArea.focus();
1616                     var cursorPos = messageTextArea.prop('selectionStart');
1617                     var currentText = messageTextArea.val();
1618                     var textBefore = currentText.substring(0, cursorPos).replace(/\S*$/, '');
1619                     var textAfter = currentText.substring(cursorPos).replace(/^\S*/, '');
1621                     messageTextArea.val(textBefore + emoji + textAfter);
1622                     // Set the cursor position to after the inserted emoji.
1623                     messageTextArea.prop('selectionStart', textBefore.length + emoji.length);
1624                     messageTextArea.prop('selectionEnd', textBefore.length + emoji.length);
1625                 }
1626             );
1627         }
1629         if (emojiPickerElement.length) {
1630             initialiseEmojiPicker(emojiPickerElement[0], function(emoji) {
1631                 var newState = StateManager.setShowEmojiPicker(viewState, !viewState.showEmojiPicker);
1632                 render(newState);
1634                 messageTextArea.focus();
1635                 var cursorPos = messageTextArea.prop('selectionStart');
1636                 var currentText = messageTextArea.val();
1637                 var textBefore = currentText.substring(0, cursorPos);
1638                 var textAfter = currentText.substring(cursorPos, currentText.length);
1640                 messageTextArea.val(textBefore + emoji + textAfter);
1641                 // Set the cursor position to after the inserted emoji.
1642                 messageTextArea.prop('selectionStart', cursorPos + emoji.length);
1643                 messageTextArea.prop('selectionEnd', cursorPos + emoji.length);
1644             });
1645         }
1647         CustomEvents.define(header, [
1648             CustomEvents.events.activate
1649         ]);
1650         CustomEvents.define(body, [
1651             CustomEvents.events.activate
1652         ]);
1653         CustomEvents.define(footer, [
1654             CustomEvents.events.activate,
1655             CustomEvents.events.enter,
1656             CustomEvents.events.escape
1657         ]);
1658         CustomEvents.define(messagesContainer, [
1659             CustomEvents.events.scrollTop,
1660             CustomEvents.events.scrollLock
1661         ]);
1663         messagesContainer.on(CustomEvents.events.scrollTop, function(e, data) {
1664             var hasMembers = Object.keys(viewState.members).length > 1;
1666             if (!isResetting && !isLoadingMoreMessages && !hasLoadedAllMessages() && hasMembers) {
1667                 isLoadingMoreMessages = true;
1668                 var newState = StateManager.setLoadingMessages(viewState, true);
1669                 render(newState);
1671                 loadMessages(viewState.id, LOAD_MESSAGE_LIMIT, getMessagesOffset(), NEWEST_FIRST, [])
1672                     .then(function() {
1673                         isLoadingMoreMessages = false;
1674                         setMessagesOffset(getMessagesOffset() + LOAD_MESSAGE_LIMIT);
1675                         return;
1676                     })
1677                     .catch(function(error) {
1678                         isLoadingMoreMessages = false;
1679                         Notification.exception(error);
1680                     });
1681             }
1683             data.originalEvent.preventDefault();
1684         });
1686         headerActivateHandlers.forEach(function(handler) {
1687             var selector = handler[0];
1688             var handlerFunction = handler[1];
1689             header.on(CustomEvents.events.activate, selector, handlerFunction);
1690         });
1692         bodyActivateHandlers.forEach(function(handler) {
1693             var selector = handler[0];
1694             var handlerFunction = handler[1];
1695             body.on(CustomEvents.events.activate, selector, handlerFunction);
1696         });
1698         footerActivateHandlers.forEach(function(handler) {
1699             var selector = handler[0];
1700             var handlerFunction = handler[1];
1701             footer.on(CustomEvents.events.activate, selector, handlerFunction);
1702         });
1704         footer.on(CustomEvents.events.enter, SELECTORS.MESSAGE_TEXT_AREA, function(e, data) {
1705             var enterToSend = footer.attr('data-enter-to-send');
1706             if (enterToSend && enterToSend != 'false' && enterToSend != '0') {
1707                 handleSendMessage(e, data);
1708             }
1709         });
1711         footer.on(CustomEvents.events.escape, SELECTORS.EMOJI_PICKER_CONTAINER, handleToggleEmojiPicker);
1712         $(document.body).on('click', handleCloseEmojiPicker);
1714         PubSub.subscribe(MessageDrawerEvents.ROUTE_CHANGED, function(newRouteData) {
1715             if (newMessagesPollTimer) {
1716                 if (newRouteData.route != MessageDrawerRoutes.VIEW_CONVERSATION) {
1717                     newMessagesPollTimer.stop();
1718                 }
1719             }
1720         });
1721     };
1723     /**
1724      * Reset the timer that polls for new messages.
1725      *
1726      * @param  {Number} conversationId The conversation id
1727      */
1728     var resetMessagePollTimer = function(conversationId) {
1729         if (newMessagesPollTimer) {
1730             newMessagesPollTimer.stop();
1731         }
1733         newMessagesPollTimer = new BackOffTimer(
1734             getLoadNewMessagesCallback(conversationId, NEWEST_FIRST),
1735             BackOffTimer.getIncrementalCallback(
1736                 viewState.messagePollMin * MILLISECONDS_IN_SEC,
1737                 MILLISECONDS_IN_SEC,
1738                 viewState.messagePollMax * MILLISECONDS_IN_SEC,
1739                 viewState.messagePollAfterMax * MILLISECONDS_IN_SEC
1740             )
1741         );
1743         newMessagesPollTimer.start();
1744     };
1746     /**
1747      * Reset the state to the initial state and render the UI.
1748      *
1749      * @param  {Object} body Conversation body container element.
1750      * @param  {Number|null} conversationId The conversation id.
1751      * @param  {Object} loggedInUserProfile The logged in user's profile.
1752      */
1753     var resetState = function(body, conversationId, loggedInUserProfile) {
1754         // Reset all of the states back to the beginning if we're loading a new
1755         // conversation.
1756         loadedAllMessages = false;
1757         messagesOffset = 0;
1758         newMessagesPollTimer = null;
1759         isRendering = false;
1760         renderBuffer = [];
1761         isResetting = true;
1762         isSendingMessage = false;
1763         sendMessageBuffer = [];
1765         var loggedInUserId = loggedInUserProfile.id;
1766         var midnight = parseInt(body.attr('data-midnight'), 10);
1767         var messagePollMin = parseInt(body.attr('data-message-poll-min'), 10);
1768         var messagePollMax = parseInt(body.attr('data-message-poll-max'), 10);
1769         var messagePollAfterMax = parseInt(body.attr('data-message-poll-after-max'), 10);
1770         var initialState = StateManager.buildInitialState(
1771             midnight,
1772             loggedInUserId,
1773             conversationId,
1774             messagePollMin,
1775             messagePollMax,
1776             messagePollAfterMax
1777         );
1779         if (!viewState) {
1780             viewState = initialState;
1781         }
1783         if (newMessagesPollTimer) {
1784             newMessagesPollTimer.stop();
1785         }
1787         render(initialState);
1788     };
1790     /**
1791      * Load a new empty private conversation between two users or self-conversation.
1792      *
1793      * @param  {Object} body Conversation body container element.
1794      * @param  {Object} loggedInUserProfile The logged in user's profile.
1795      * @param  {Int} otherUserId The other user's id.
1796      * @return {Promise} Renderer promise.
1797      */
1798     var resetNoConversation = function(body, loggedInUserProfile, otherUserId) {
1799         // Always reset the state back to the initial state so that the
1800         // state manager and patcher can work correctly.
1801         resetState(body, null, loggedInUserProfile);
1803         var resetNoConversationPromise = null;
1805         if (loggedInUserProfile.id != otherUserId) {
1806             // Private conversation between two different users.
1807             resetNoConversationPromise = Repository.getConversationBetweenUsers(
1808                 loggedInUserProfile.id,
1809                 otherUserId,
1810                 true,
1811                 true,
1812                 0,
1813                 0,
1814                 LOAD_MESSAGE_LIMIT,
1815                 0,
1816                 NEWEST_FIRST
1817             );
1818         } else {
1819             // Self conversation.
1820             resetNoConversationPromise = Repository.getSelfConversation(
1821                 loggedInUserProfile.id,
1822                 LOAD_MESSAGE_LIMIT,
1823                 0,
1824                 NEWEST_FIRST
1825             );
1826         }
1828         return resetNoConversationPromise.then(function(conversation) {
1829                 // Looks like we have a conversation after all! Let's use that.
1830                 return resetByConversation(body, conversation, loggedInUserProfile);
1831             })
1832             .catch(function() {
1833                 // Can't find a conversation. Oh well. Just load up a blank one.
1834                 return loadEmptyPrivateConversation(loggedInUserProfile, otherUserId);
1835             });
1836     };
1838     /**
1839      * Load new messages into the conversation based on a time interval.
1840      *
1841      * @param  {Object} body Conversation body container element.
1842      * @param  {Number} conversationId The conversation id.
1843      * @param  {Object} loggedInUserProfile The logged in user's profile.
1844      * @return {Promise} Renderer promise.
1845      */
1846     var resetById = function(body, conversationId, loggedInUserProfile) {
1847         var cache = null;
1848         if (conversationId in stateCache) {
1849             cache = stateCache[conversationId];
1850         }
1852         // Always reset the state back to the initial state so that the
1853         // state manager and patcher can work correctly.
1854         resetState(body, conversationId, loggedInUserProfile);
1856         var promise = $.Deferred().resolve({}).promise();
1857         if (cache) {
1858             // We've seen this conversation before so there is no need to
1859             // send any network requests.
1860             var newState = cache.state;
1861             // Reset some loading states just in case they were left weirdly.
1862             newState = StateManager.setLoadingMessages(newState, false);
1863             newState = StateManager.setLoadingMembers(newState, false);
1864             setMessagesOffset(cache.messagesOffset);
1865             setLoadedAllMessages(cache.loadedAllMessages);
1866             render(newState);
1867         } else {
1868             promise = loadNewConversation(
1869                 conversationId,
1870                 loggedInUserProfile,
1871                 LOAD_MESSAGE_LIMIT,
1872                 0,
1873                 NEWEST_FIRST
1874             );
1875         }
1877         return promise.then(function() {
1878             return resetMessagePollTimer(conversationId);
1879         });
1880     };
1882     /**
1883      * Load new messages into the conversation based on a time interval.
1884      *
1885      * @param  {Object} body Conversation body container element.
1886      * @param  {Object} conversation The conversation.
1887      * @param  {Object} loggedInUserProfile The logged in user's profile.
1888      * @return {Promise} Renderer promise.
1889      */
1890     var resetByConversation = function(body, conversation, loggedInUserProfile) {
1891         var cache = null;
1892         if (conversation.id in stateCache) {
1893             cache = stateCache[conversation.id];
1894         }
1896         // Always reset the state back to the initial state so that the
1897         // state manager and patcher can work correctly.
1898         resetState(body, conversation.id, loggedInUserProfile);
1900         var promise = $.Deferred().resolve({}).promise();
1901         if (cache) {
1902             // We've seen this conversation before so there is no need to
1903             // send any network requests.
1904             var newState = cache.state;
1905             // Reset some loading states just in case they were left weirdly.
1906             newState = StateManager.setLoadingMessages(newState, false);
1907             newState = StateManager.setLoadingMembers(newState, false);
1908             setMessagesOffset(cache.messagesOffset);
1909             setLoadedAllMessages(cache.loadedAllMessages);
1910             render(newState);
1911         } else {
1912             promise = loadExistingConversation(
1913                 conversation,
1914                 loggedInUserProfile,
1915                 LOAD_MESSAGE_LIMIT,
1916                 NEWEST_FIRST
1917             );
1918         }
1920         return promise.then(function() {
1921             return resetMessagePollTimer(conversation.id);
1922         });
1923     };
1925     /**
1926      * Setup the conversation page. This is a rather complex function because there are a
1927      * few combinations of arguments that can be provided to this function to show the
1928      * conversation.
1929      *
1930      * There are:
1931      * 1.) A conversation object with no action or other user id (e.g. from the overview page)
1932      * 2.) A conversation id with no action or other user id (e.g. from the contacts page)
1933      * 3.) No conversation/id with an action and other other user id. (e.g. from contact page)
1934      *
1935      * @param {string} namespace The route namespace.
1936      * @param {Object} header Conversation header container element.
1937      * @param {Object} body Conversation body container element.
1938      * @param {Object} footer Conversation footer container element.
1939      * @param {Object|Number|null} conversationOrId Conversation or id or null
1940      * @param {String} action An action to take on the conversation
1941      * @param {Number} otherUserId The other user id for a private conversation
1942      * @return {Object} jQuery promise
1943      */
1944     var show = function(namespace, header, body, footer, conversationOrId, action, otherUserId) {
1945         var conversation = null;
1946         var conversationId = null;
1948         // Check what we were given to identify the conversation.
1949         if (conversationOrId && conversationOrId !== null && typeof conversationOrId == 'object') {
1950             conversation = conversationOrId;
1951             conversationId = parseInt(conversation.id, 10);
1952         } else {
1953             conversation = null;
1954             conversationId = parseInt(conversationOrId, 10);
1955             conversationId = isNaN(conversationId) ? null : conversationId;
1956         }
1958         if (!conversationId && action && otherUserId) {
1959             // If we didn't get a conversation id got a user id then let's see if we've
1960             // previously loaded a private conversation with this user.
1961             conversationId = getCachedPrivateConversationIdFromUserId(otherUserId);
1962         }
1964         // This is a new conversation if:
1965         // 1. We don't already have a state
1966         // 2. The given conversation doesn't match the one currently loaded
1967         // 3. We have a view state without a conversation id and we weren't given one
1968         //    but we were given a different other user id. This happens when the user
1969         //    goes from viewing a user that they haven't yet initialised a conversation
1970         //    with to viewing a different user that they also haven't initialised a
1971         //    conversation with.
1972         var isNewConversation = !viewState || (viewState.id != conversationId) || (otherUserId && otherUserId != getOtherUserId());
1974         if (!body.attr('data-init')) {
1975             // Generate the render function to bind the header, body, and footer
1976             // elements to it so that we don't need to pass them around this module.
1977             render = generateRenderFunction(header, body, footer, isNewConversation);
1978             registerEventListeners(namespace, header, body, footer);
1979             body.attr('data-init', true);
1980         }
1982         if (isNewConversation) {
1983             var renderPromise = null;
1984             var loggedInUserProfile = getLoggedInUserProfile(body);
1986             if (conversation) {
1987                 renderPromise = resetByConversation(body, conversation, loggedInUserProfile, otherUserId);
1988             } else if (conversationId) {
1989                 renderPromise = resetById(body, conversationId, loggedInUserProfile, otherUserId);
1990             } else {
1991                 renderPromise = resetNoConversation(body, loggedInUserProfile, otherUserId);
1992             }
1994             return renderPromise
1995                 .then(function() {
1996                     isResetting = false;
1997                     // Focus the first element that can receieve it in the header.
1998                     header.find(Constants.SELECTORS.CAN_RECEIVE_FOCUS).first().focus();
1999                     return;
2000                 })
2001                 .catch(function(error) {
2002                     isResetting = false;
2003                     Notification.exception(error);
2004                 });
2005         }
2007         // We're not loading a new conversation so we should reset the poll timer to try to load
2008         // new messages.
2009         resetMessagePollTimer(conversationId);
2011         if (viewState.type == CONVERSATION_TYPES.PRIVATE && action) {
2012             // There are special actions that the user can perform in a private (aka 1-to-1)
2013             // conversation.
2014             var currentOtherUserId = getOtherUserId();
2016             switch (action) {
2017                 case 'block':
2018                     return requestBlockUser(currentOtherUserId);
2019                 case 'unblock':
2020                     return requestUnblockUser(currentOtherUserId);
2021                 case 'add-contact':
2022                     return requestAddContact(currentOtherUserId);
2023                 case 'remove-contact':
2024                     return requestRemoveContact(currentOtherUserId);
2025             }
2026         }
2028         // Final fallback to return a promise if we didn't need to do anything.
2029         return $.Deferred().resolve().promise();
2030     };
2032     /**
2033      * String describing this page used for aria-labels.
2034      *
2035      * @return {Object} jQuery promise
2036      */
2037     var description = function() {
2038         return Str.get_string('messagedrawerviewconversation', 'core_message', viewState.name);
2039     };
2041     return {
2042         show: show,
2043         description: description
2044     };
2045 });