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