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