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