Merge branch 'MDL-57338-master' of git://github.com/danpoltawski/moodle
[moodle.git] / message / amd / src / message_area_messages.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * This module handles the message area of the messaging area.
18  *
19  * @module     core_message/message_area_messages
20  * @package    core_message
21  * @copyright  2016 Mark Nelson <markn@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/custom_interaction_events',
25         'core/auto_rows', 'core_message/message_area_actions', 'core/modal_factory', 'core/modal_events',
26         'core/str', 'core_message/message_area_events', 'core/backoff_timer'],
27     function($, Ajax, Templates, Notification, CustomEvents, AutoRows, Actions, ModalFactory,
28              ModalEvents, Str, Events, BackOffTimer) {
30         /** @type {int} The message area default height. */
31         var MESSAGES_AREA_DEFAULT_HEIGHT = 500;
33         /** @type {int} The response default height. */
34         var MESSAGES_RESPONSE_DEFAULT_HEIGHT = 50;
36         /** @type {Object} The list of selectors for the message area. */
37         var SELECTORS = {
38             BLOCKTIME: "[data-region='blocktime']",
39             CANCELDELETEMESSAGES: "[data-action='cancel-delete-messages']",
40             CONTACT: "[data-region='contact']",
41             CONVERSATIONS: "[data-region='contacts'][data-region-content='conversations']",
42             DELETEALLMESSAGES: "[data-action='delete-all-messages']",
43             DELETEMESSAGES: "[data-action='delete-messages']",
44             LOADINGICON: '.loading-icon',
45             MESSAGE: "[data-region='message']",
46             MESSAGERESPONSE: "[data-region='response']",
47             MESSAGES: "[data-region='messages']",
48             MESSAGESAREA: "[data-region='messages-area']",
49             MESSAGINGAREA: "[data-region='messaging-area']",
50             SENDMESSAGE: "[data-action='send-message']",
51             SENDMESSAGETEXT: "[data-region='send-message-txt']",
52             SHOWCONTACTS: "[data-action='show-contacts']",
53             STARTDELETEMESSAGES: "[data-action='start-delete-messages']",
54         };
56         /** @type {int} The number of milliseconds in a second. */
57         var MILLISECONDSINSEC = 1000;
59         /**
60          * Messages class.
61          *
62          * @param {Messagearea} messageArea The messaging area object.
63          */
64         function Messages(messageArea) {
65             this.messageArea = messageArea;
66             this._init();
67         }
69         /** @type {Boolean} checks if we are sending a message */
70         Messages.prototype._isSendingMessage = false;
72         /** @type {Boolean} checks if we are currently loading messages */
73         Messages.prototype._isLoadingMessages = false;
75         /** @type {int} the number of messagess displayed */
76         Messages.prototype._numMessagesDisplayed = 0;
78         /** @type {array} the messages displayed or about to be displayed on the page */
79         Messages.prototype._messageQueue = [];
81         /** @type {int} the number of messages to retrieve */
82         Messages.prototype._numMessagesToRetrieve = 20;
84         /** @type {Modal} the confirmation modal */
85         Messages.prototype._confirmationModal = null;
87         /** @type {int} the timestamp for the most recent visible message */
88         Messages.prototype._latestMessageTimestamp = 0;
90         /** @type {BackOffTimer} the backoff timer */
91         Messages.prototype._backoffTimer = null;
93         /** @type {Messagearea} The messaging area object. */
94         Messages.prototype.messageArea = null;
96         /**
97          * Initialise the event listeners.
98          *
99          * @private
100          */
101         Messages.prototype._init = function() {
102             CustomEvents.define(this.messageArea.node, [
103                 CustomEvents.events.activate,
104                 CustomEvents.events.up,
105                 CustomEvents.events.down,
106                 CustomEvents.events.enter,
107             ]);
109             // We have a responsive media query based on height that reduces this size on screens shorter than 670.
110             if ($(window).height() <= 670) {
111                 MESSAGES_AREA_DEFAULT_HEIGHT = 400;
112             }
114             AutoRows.init(this.messageArea.node);
116             this.messageArea.onCustomEvent(Events.CONVERSATIONSELECTED, this._viewMessages.bind(this));
117             this.messageArea.onCustomEvent(Events.SENDMESSAGE, this._viewMessages.bind(this));
118             this.messageArea.onCustomEvent(Events.CHOOSEMESSAGESTODELETE, this._chooseMessagesToDelete.bind(this));
119             this.messageArea.onCustomEvent(Events.CANCELDELETEMESSAGES, this._hideDeleteAction.bind(this));
120             this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.SENDMESSAGE,
121                 this._sendMessage.bind(this));
122             this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.STARTDELETEMESSAGES,
123                 this._startDeleting.bind(this));
124             this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.DELETEMESSAGES,
125                 this._deleteMessages.bind(this));
126             this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.DELETEALLMESSAGES,
127                 this._deleteAllMessages.bind(this));
128             this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.CANCELDELETEMESSAGES,
129                 this._triggerCancelMessagesToDelete.bind(this));
130             this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.MESSAGE,
131                 this._toggleMessage.bind(this));
132             this.messageArea.onDelegateEvent(CustomEvents.events.activate, SELECTORS.SHOWCONTACTS,
133                 this._hideMessagingArea.bind(this));
135             this.messageArea.onDelegateEvent(CustomEvents.events.up, SELECTORS.MESSAGE,
136                 this._selectPreviousMessage.bind(this));
137             this.messageArea.onDelegateEvent(CustomEvents.events.down, SELECTORS.MESSAGE,
138                 this._selectNextMessage.bind(this));
140             this.messageArea.onDelegateEvent('focus', SELECTORS.SENDMESSAGETEXT, this._setMessaging.bind(this));
141             this.messageArea.onDelegateEvent('blur', SELECTORS.SENDMESSAGETEXT, this._clearMessaging.bind(this));
143             this.messageArea.onDelegateEvent(CustomEvents.events.enter, SELECTORS.SENDMESSAGETEXT,
144                 this._sendMessageHandler.bind(this));
146             $(document).on(AutoRows.events.ROW_CHANGE, this._adjustMessagesAreaHeight.bind(this));
148             // Check if any messages have been displayed on page load.
149             var messages = this.messageArea.find(SELECTORS.MESSAGES);
150             if (messages.length) {
151                 this._addScrollEventListener(messages.find(SELECTORS.MESSAGE).length);
152                 this._latestMessageTimestamp = messages.find(SELECTORS.MESSAGE + ':last').data('timecreated');
153             }
155             // Create a timer to poll the server for new messages.
156             this._backoffTimer = new BackOffTimer(this._loadNewMessages.bind(this),
157                 BackOffTimer.getIncrementalCallback(this.messageArea.pollmin * MILLISECONDSINSEC, MILLISECONDSINSEC,
158                     this.messageArea.pollmax * MILLISECONDSINSEC, this.messageArea.polltimeout * MILLISECONDSINSEC));
160             // Start the timer.
161             this._backoffTimer.start();
162         };
164         /**
165          * View the message panel.
166          *
167          * @param {Event} event
168          * @param {int} userid
169          * @return {Promise} The promise resolved when the messages have been loaded.
170          * @private
171          */
172         Messages.prototype._viewMessages = function(event, userid) {
173             // We are viewing another user, or re-loading the panel, so set number of messages displayed to 0.
174             this._numMessagesDisplayed = 0;
175             // Stop the existing timer so we can set up the new user's messages.
176             this._backoffTimer.stop();
177             // Reset the latest timestamp when we change the messages view.
178             this._latestMessageTimestamp = 0;
180             // Mark all the messages as read.
181             var markMessagesAsRead = Ajax.call([{
182                 methodname: 'core_message_mark_all_messages_as_read',
183                 args: {
184                     useridto: this.messageArea.getCurrentUserId(),
185                     useridfrom: userid
186                 }
187             }]);
189             // Keep track of the number of messages received.
190             var numberreceived = 0;
191             // Show loading template.
192             return Templates.render('core/loading', {}).then(function(html, js) {
193                 Templates.replaceNodeContents(this.messageArea.find(SELECTORS.MESSAGESAREA), html, js);
194                 return markMessagesAsRead[0];
195             }.bind(this)).then(function() {
196                 var conversationnode = this.messageArea.find(SELECTORS.CONVERSATIONS + " " +
197                     SELECTORS.CONTACT + "[data-userid='" + userid + "']");
198                 if (conversationnode.hasClass('unread')) {
199                     // Remove the class.
200                     conversationnode.removeClass('unread');
201                     // Trigger an event letting the notification popover (and whoever else) know.
202                     $(document).trigger('messagearea:conversationselected', userid);
203                 }
204                 return this._getMessages(userid);
205             }.bind(this)).then(function(data) {
206                 numberreceived = data.messages.length;
207                 // We have the data - lets render the template with it.
208                 return Templates.render('core_message/message_area_messages_area', data);
209             }).then(function(html, js) {
210                 Templates.replaceNodeContents(this.messageArea.find(SELECTORS.MESSAGESAREA), html, js);
211                 this._addScrollEventListener(numberreceived);
212                 // Restart the poll timer.
213                 this._backoffTimer.restart();
214                 this.messageArea.find(SELECTORS.SENDMESSAGETEXT).focus();
215             }.bind(this)).fail(Notification.exception);
216         };
218         /**
219          * Loads messages while scrolling.
220          *
221          * @return {Promise|boolean} The promise resolved when the messages have been loaded.
222          * @private
223          */
224         Messages.prototype._loadMessages = function() {
225             if (this._isLoadingMessages) {
226                 return false;
227             }
229             this._isLoadingMessages = true;
231             // Keep track of the number of messages received.
232             var numberreceived = 0;
233             // Show loading template.
234             return Templates.render('core/loading', {}).then(function(html, js) {
235                 Templates.prependNodeContents(this.messageArea.find(SELECTORS.MESSAGES),
236                     "<div style='text-align:center'>" + html + "</div>", js);
237                 return this._getMessages(this._getUserId());
238             }.bind(this)).then(function(data) {
239                 numberreceived = data.messages.length;
240                 // We have the data - lets render the template with it.
241                 return Templates.render('core_message/message_area_messages', data);
242             }).then(function(html, js) {
243                 // Remove the loading icon.
244                 this.messageArea.find(SELECTORS.MESSAGES + " " +
245                     SELECTORS.LOADINGICON).remove();
246                 // Check if we got something to do.
247                 if (numberreceived > 0) {
248                     var newHtml = $('<div>' + html + '</div>');
249                     if (this._hasMatchingBlockTime(this.messageArea.node, newHtml, true)) {
250                         this.messageArea.node.find(SELECTORS.BLOCKTIME + ':first').remove();
251                     }
252                     // Get height before we add the messages.
253                     var oldheight = this.messageArea.find(SELECTORS.MESSAGES)[0].scrollHeight;
254                     // Show the new content.
255                     Templates.prependNodeContents(this.messageArea.find(SELECTORS.MESSAGES), html, js);
256                     // Get height after we add the messages.
257                     var newheight = this.messageArea.find(SELECTORS.MESSAGES)[0].scrollHeight;
258                     // Make sure scroll bar is at the location before we loaded more messages.
259                     this.messageArea.find(SELECTORS.MESSAGES).scrollTop(newheight - oldheight);
260                     // Increment the number of messages displayed.
261                     this._numMessagesDisplayed += numberreceived;
262                 }
263                 // Mark that we are no longer busy loading data.
264                 this._isLoadingMessages = false;
265             }.bind(this)).fail(Notification.exception);
266         };
268         /**
269          * Loads and renders messages newer than the most recently seen messages.
270          *
271          * @return {Promise|boolean} The promise resolved when the messages have been loaded.
272          * @private
273          */
274         Messages.prototype._loadNewMessages = function() {
275             if (this._isLoadingMessages) {
276                 return false;
277             }
279             // If we have no user id yet then bail early.
280             if (!this._getUserId()) {
281                 return false;
282             }
284             this._isLoadingMessages = true;
286             // Only scroll the message window if the user hasn't scrolled up.
287             var shouldScrollBottom = false;
288             var messages = this.messageArea.find(SELECTORS.MESSAGES);
289             if (messages.length !== 0) {
290                 var scrollTop = messages.scrollTop();
291                 var innerHeight = messages.innerHeight();
292                 var scrollHeight = messages[0].scrollHeight;
294                 if (scrollTop + innerHeight >= scrollHeight) {
295                     shouldScrollBottom = true;
296                 }
297             }
299             // Keep track of the number of messages received.
300             return this._getMessages(this._getUserId(), true).then(function(data) {
301                 return this._addMessagesToDom(data.messages, shouldScrollBottom);
302             }.bind(this)).always(function() {
303                 // Mark that we are no longer busy loading data.
304                 this._isLoadingMessages = false;
305             }.bind(this)).fail(Notification.exception);
306         };
308         /**
309          * Handles returning a list of messages to display.
310          *
311          * @param {int} userid
312          * @param {bool} fromTimestamp Load messages from the latest known timestamp
313          * @return {Promise} The promise resolved when the contact area has been rendered
314          * @private
315          */
316         Messages.prototype._getMessages = function(userid, fromTimestamp) {
317             var args = {
318                 currentuserid: this.messageArea.getCurrentUserId(),
319                 otheruserid: userid,
320                 limitfrom: this._numMessagesDisplayed,
321                 limitnum: this._numMessagesToRetrieve,
322                 newest: true
323             };
325             // If we're trying to load new messages since the message UI was
326             // rendered. Used for ajax polling while user is on the message UI.
327             if (fromTimestamp) {
328                 args.timefrom = this._latestMessageTimestamp;
329                 // Remove limit and offset. We want all new messages.
330                 args.limitfrom = 0;
331                 args.limitnum = 0;
332             }
334             // Call the web service to get our data.
335             var promises = Ajax.call([{
336                 methodname: 'core_message_data_for_messagearea_messages',
337                 args: args,
338             }]);
340             // Do stuff when we get data back.
341             return promises[0].then(function(data) {
342                 var messages = data.messages;
344                 // Did we get any new messages?
345                 if (messages && messages.length) {
346                     var latestMessage = messages[messages.length - 1];
348                     // Update our record of the latest known message for future requests.
349                     if (latestMessage.timecreated > this._latestMessageTimestamp) {
350                         // Next request should be for the second after the most recent message we've seen.
351                         this._latestMessageTimestamp = latestMessage.timecreated + 1;
352                     }
353                 }
355                 return data;
356             }.bind(this)).fail(function(ex) {
357                 // Stop the timer if we received an error so that we don't keep spamming the server.
358                 this._backoffTimer.stop();
359                 Notification.exception(ex);
360             }.bind(this));
361         };
363         /**
364          * Handles sending a message.
365          *
366          * @return {Promise|boolean} The promise resolved once the message has been sent.
367          * @private
368          */
369         Messages.prototype._sendMessage = function() {
370             var element = this.messageArea.find(SELECTORS.SENDMESSAGETEXT);
371             var text = element.val().trim();
373             // Do not do anything if it is empty.
374             if (text === '') {
375                 return false;
376             }
378             // If we are sending a message, don't do anything, be patient!
379             if (this._isSendingMessage) {
380                 return false;
381             }
383             // Ok, mark that we are sending a message.
384             this._isSendingMessage = true;
386             // Call the web service to save our message.
387             var promises = Ajax.call([{
388                 methodname: 'core_message_send_instant_messages',
389                 args: {
390                     messages: [
391                         {
392                             touserid: this._getUserId(),
393                             text: text
394                         }
395                     ]
396                 }
397             }]);
399             element.prop('disabled', true);
401             // Update the DOM when we get some data back.
402             return promises[0].then(function(response) {
403                 if (response.length < 0) {
404                     // Even errors should return valid data.
405                     throw new Error('Invalid response');
406                 }
407                 if (response[0].errormessage) {
408                     throw new Error(response[0].errormessage);
409                 }
410                 // Fire an event to say the message was sent.
411                 this.messageArea.trigger(Events.MESSAGESENT, [this._getUserId(), text]);
412                 // Update the messaging area.
413                 return this._addLastMessageToDom();
414             }.bind(this)).then(function() {
415                 // Ok, we are no longer sending a message.
416                 this._isSendingMessage = false;
417             }.bind(this)).always(function() {
418                 element.prop('disabled', false);
419                 element.focus();
420             }).fail(Notification.exception);
421         };
423         /**
424          * Handles selecting messages to delete.
425          *
426          * @private
427          */
428         Messages.prototype._chooseMessagesToDelete = function() {
429             this.messageArea.find(SELECTORS.MESSAGESAREA).addClass('editing');
430             this.messageArea.find(SELECTORS.MESSAGE)
431                 .attr('role', 'checkbox')
432                 .attr('aria-checked', 'false');
433         };
435         /**
436          * Handles deleting messages.
437          *
438          * @private
439          */
440         Messages.prototype._deleteMessages = function() {
441             var userid = this.messageArea.getCurrentUserId();
442             var checkboxes = this.messageArea.find(SELECTORS.MESSAGE + "[aria-checked='true']");
443             var requests = [];
444             var messagestoremove = [];
446             // Go through all the checked checkboxes and prepare them for deletion.
447             checkboxes.each(function(id, element) {
448                 var node = $(element);
449                 var messageid = node.data('messageid');
450                 var isread = node.data('messageread') ? 1 : 0;
451                 messagestoremove.push(node);
452                 requests.push({
453                     methodname: 'core_message_delete_message',
454                     args: {
455                         messageid: messageid,
456                         userid: userid,
457                         read: isread
458                     }
459                 });
460             });
462             if (requests.length > 0) {
463                 Ajax.call(requests)[requests.length - 1].then(function() {
464                     // Store the last message on the page, and the last message being deleted.
465                     var updatemessage = null;
466                     var messages = this.messageArea.find(SELECTORS.MESSAGE);
467                     var lastmessage = messages.last();
468                     var lastremovedmessage = messagestoremove[messagestoremove.length - 1];
469                     // Remove the messages from the DOM.
470                     $.each(messagestoremove, function(key, message) {
471                         // Remove the message.
472                         message.remove();
473                     });
474                     // If the last message was deleted then we need to provide the new last message.
475                     if (lastmessage.data('id') === lastremovedmessage.data('id')) {
476                         updatemessage = this.messageArea.find(SELECTORS.MESSAGE).last();
477                     }
478                     // Now we have removed all the messages from the DOM lets remove any block times we may need to as well.
479                     $.each(messagestoremove, function(key, message) {
480                         // First - let's make sure there are no more messages in that time block.
481                         var blocktime = message.data('blocktime');
482                         if (this.messageArea.find(SELECTORS.MESSAGE +
483                             "[data-blocktime='" + blocktime + "']").length === 0) {
484                             this.messageArea.find(SELECTORS.BLOCKTIME +
485                                 "[data-blocktime='" + blocktime + "']").remove();
486                         }
487                     }.bind(this));
489                     // If there are no messages at all, then remove conversation panel.
490                     if (this.messageArea.find(SELECTORS.MESSAGE).length === 0) {
491                         this.messageArea.find(SELECTORS.CONVERSATIONS + " " +
492                             SELECTORS.CONTACT + "[data-userid='" + this._getUserId() + "']").remove();
493                     }
495                     // Trigger event letting other modules know messages were deleted.
496                     this.messageArea.trigger(Events.MESSAGESDELETED, [this._getUserId(), updatemessage]);
497                 }.bind(this), Notification.exception);
498             } else {
499                 // Trigger event letting other modules know messages were deleted.
500                 this.messageArea.trigger(Events.MESSAGESDELETED, this._getUserId());
501             }
503             // Hide the items responsible for deleting messages.
504             this._hideDeleteAction();
505         };
507         /**
508          * Handles adding a scrolling event listener.
509          *
510          * @param {int} numberreceived The number of messages received
511          * @private
512          */
513         Messages.prototype._addScrollEventListener = function(numberreceived) {
514             // Scroll to the bottom.
515             this._scrollBottom();
516             // Set the number of messages displayed.
517             this._numMessagesDisplayed = numberreceived;
518             // Now enable the ability to infinitely scroll through messages.
519             CustomEvents.define(this.messageArea.find(SELECTORS.MESSAGES), [
520                 CustomEvents.events.scrollTop
521             ]);
522             // Assign the event for scrolling.
523             this.messageArea.onCustomEvent(CustomEvents.events.scrollTop, this._loadMessages.bind(this));
524         };
526         /**
527          * Handles deleting a conversation.
528          *
529          * @private
530          */
531         Messages.prototype._deleteAllMessages = function() {
532             // Create the confirmation modal if we haven't already.
533             if (!this._confirmationModal) {
534                 Str.get_strings([
535                     {key: 'confirm'},
536                     {key: 'deleteallconfirm', component: 'message'}
537                 ]).done(function(s) {
538                     ModalFactory.create({
539                         title: s[0],
540                         type: ModalFactory.types.CONFIRM,
541                         body: s[1]
542                     }, this.messageArea.find(SELECTORS.DELETEALLMESSAGES))
543                         .done(function(modal) {
544                             this._confirmationModal = modal;
546                             // Only delete the conversation if the user agreed in the confirmation modal.
547                             modal.getRoot().on(ModalEvents.yes, function() {
548                                 var otherUserId = this._getUserId();
549                                 var request = {
550                                     methodname: 'core_message_delete_conversation',
551                                     args: {
552                                         userid: this.messageArea.getCurrentUserId(),
553                                         otheruserid: otherUserId
554                                     }
555                                 };
557                                 // Delete the conversation.
558                                 Ajax.call([request])[0].then(function() {
559                                     // Clear the message area.
560                                     this.messageArea.find(SELECTORS.MESSAGESAREA).empty();
561                                     // Let the app know a conversation was deleted.
562                                     this.messageArea.trigger(Events.CONVERSATIONDELETED, otherUserId);
563                                     this._hideDeleteAction();
564                                 }.bind(this), Notification.exception);
565                             }.bind(this));
567                             // Display the confirmation.
568                             modal.show();
569                         }.bind(this));
570                 }.bind(this));
571             } else {
572                 // Otherwise just show the existing modal.
573                 this._confirmationModal.show();
574             }
575         };
577         /**
578          * Handles hiding the delete checkboxes and replacing the response area.
579          *
580          * @private
581          */
582         Messages.prototype._hideDeleteAction = function() {
583             this.messageArea.find(SELECTORS.MESSAGE)
584                 .removeAttr('role')
585                 .removeAttr('aria-checked');
586             this.messageArea.find(SELECTORS.MESSAGESAREA).removeClass('editing');
587         };
589         /**
590          * Triggers the CANCELDELETEMESSAGES event.
591          *
592          * @private
593          */
594         Messages.prototype._triggerCancelMessagesToDelete = function() {
595             // Trigger event letting other modules know message deletion was canceled.
596             this.messageArea.trigger(Events.CANCELDELETEMESSAGES);
597         };
599         /**
600          * Handles adding messages to the DOM.
601          *
602          * @param {array} messages An array of messages to be added to the DOM.
603          * @param {boolean} shouldScrollBottom True will scroll to the bottom of the message window and show the new messages.
604          * @return {Promise} The promise resolved when the messages have been added to the DOM.
605          * @private
606          */
607         Messages.prototype._addMessagesToDom = function(messages, shouldScrollBottom) {
608             var numberreceived = 0;
609             var messagesArea = this.messageArea.find(SELECTORS.MESSAGES);
610             messages = messages.filter(function(message) {
611                 var id = "" + message.id + message.isread;
612                 // If the message is already queued to be rendered, remove from the list of messages.
613                 if (this._messageQueue[id]) {
614                     return false;
615                 }
616                 // Filter out any messages already rendered.
617                 var result = messagesArea.find(SELECTORS.MESSAGE + '[data-id="' + id + '"]');
618                 // Any message we are rendering should go in the messageQueue.
619                 if (!result.length) {
620                     this._messageQueue[id] = true;
621                 }
622                 return !result.length;
623             }.bind(this));
624             numberreceived = messages.length;
625             // We have the data - lets render the template with it.
626             return Templates.render('core_message/message_area_messages', {messages: messages}).then(function(html, js) {
627                 // Check if we got something to do.
628                 if (numberreceived > 0) {
629                     var newHtml = $('<div>' + html + '</div>');
630                     if (this._hasMatchingBlockTime(this.messageArea.node, newHtml, false)) {
631                         newHtml.find(SELECTORS.BLOCKTIME + ':first').remove();
632                     }
633                     // Show the new content.
634                     Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), newHtml, js);
635                     // Scroll the new message into view.
636                     if (shouldScrollBottom) {
637                         this._scrollBottom();
638                     }
639                     // Increment the number of messages displayed.
640                     this._numMessagesDisplayed += numberreceived;
641                     // Reset the poll timer because the user may be active.
642                     this._backoffTimer.restart();
643                 }
644             }.bind(this));
645         };
647         /**
648          * Handles adding the last message to the DOM.
649          *
650          * @return {Promise} The promise resolved when the message has been added to the DOM.
651          * @private
652          */
653         Messages.prototype._addLastMessageToDom = function() {
654             // Call the web service to return how the message should look.
655             var promises = Ajax.call([{
656                 methodname: 'core_message_data_for_messagearea_get_most_recent_message',
657                 args: {
658                     currentuserid: this.messageArea.getCurrentUserId(),
659                     otheruserid: this._getUserId()
660                 }
661             }]);
663             // Add the message.
664             return promises[0].then(function(data) {
665                 return this._addMessagesToDom([data], true);
666             }.bind(this)).always(function() {
667                 // Empty the response text area.text
668                 this.messageArea.find(SELECTORS.SENDMESSAGETEXT).val('').trigger('input');
669             }.bind(this)).fail(Notification.exception);
670         };
672         /**
673          * Returns the ID of the other user in the conversation.
674          *
675          * @return {int} The user id
676          * @private
677          */
678         Messages.prototype._getUserId = function() {
679             return this.messageArea.find(SELECTORS.MESSAGES).data('userid');
680         };
682         /**
683          * Scrolls to the bottom of the messages.
684          *
685          * @private
686          */
687         Messages.prototype._scrollBottom = function() {
688             // Scroll to the bottom.
689             var messages = this.messageArea.find(SELECTORS.MESSAGES);
690             if (messages.length !== 0) {
691                 messages.scrollTop(messages[0].scrollHeight);
692             }
693         };
695         /**
696          * Select the previous message in the list.
697          *
698          * @param {event} e The jquery event
699          * @param {object} data Extra event data
700          * @private
701          */
702         Messages.prototype._selectPreviousMessage = function(e, data) {
703             var currentMessage = $(e.target).closest(SELECTORS.MESSAGE);
705             do {
706                 currentMessage = currentMessage.prev();
707             } while (currentMessage.length && !currentMessage.is(SELECTORS.MESSAGE));
709             currentMessage.focus();
711             data.originalEvent.preventDefault();
712             data.originalEvent.stopPropagation();
713         };
715         /**
716          * Select the next message in the list.
717          *
718          * @param {event} e The jquery event
719          * @param {object} data Extra event data
720          * @private
721          */
722         Messages.prototype._selectNextMessage = function(e, data) {
723             var currentMessage = $(e.target).closest(SELECTORS.MESSAGE);
725             do {
726                 currentMessage = currentMessage.next();
727             } while (currentMessage.length && !currentMessage.is(SELECTORS.MESSAGE));
729             currentMessage.focus();
731             data.originalEvent.preventDefault();
732             data.originalEvent.stopPropagation();
733         };
735         /**
736          * Flag the response area as messaging.
737          *
738          * @param {event} e The jquery event
739          * @private
740          */
741         Messages.prototype._setMessaging = function(e) {
742             $(e.target).closest(SELECTORS.MESSAGERESPONSE).addClass('messaging');
743         };
745         /**
746          * Clear the response area as messaging flag.
747          *
748          * @param {event} e The jquery event
749          * @private
750          */
751         Messages.prototype._clearMessaging = function(e) {
752             $(e.target).closest(SELECTORS.MESSAGERESPONSE).removeClass('messaging');
753         };
755         /**
756          * Turn on delete message mode.
757          *
758          * @param {event} e The jquery event
759          * @private
760          */
761         Messages.prototype._startDeleting = function(e) {
762             var actions = new Actions(this.messageArea);
763             actions.chooseMessagesToDelete();
765             e.preventDefault();
766         };
768         /**
769          * Check if the message area is in editing mode.
770          *
771          * @return {bool}
772          * @private
773          */
774         Messages.prototype._isEditing = function() {
775             return this.messageArea.find(SELECTORS.MESSAGESAREA).hasClass('editing');
776         };
778         /**
779          * Check or uncheck the message if the message area is in editing mode.
780          *
781          * @param {event} e The jquery event
782          * @private
783          */
784         Messages.prototype._toggleMessage = function(e) {
785             if (!this._isEditing()) {
786                 return;
787             }
789             var message = $(e.target).closest(SELECTORS.MESSAGE);
791             if (message.attr('aria-checked') === 'true') {
792                 message.attr('aria-checked', 'false');
793             } else {
794                 message.attr('aria-checked', 'true');
795             }
796         };
798         /**
799          * Adjust the height of the messages area to match the changed height of
800          * the response area.
801          *
802          * @private
803          */
804         Messages.prototype._adjustMessagesAreaHeight = function() {
805             var messagesArea = this.messageArea.find(SELECTORS.MESSAGES);
806             var messagesResponse = this.messageArea.find(SELECTORS.MESSAGERESPONSE);
808             var currentMessageResponseHeight = messagesResponse.outerHeight();
809             var diffResponseHeight = currentMessageResponseHeight - MESSAGES_RESPONSE_DEFAULT_HEIGHT;
810             var newMessagesAreaHeight = MESSAGES_AREA_DEFAULT_HEIGHT - diffResponseHeight;
812             messagesArea.outerHeight(newMessagesAreaHeight);
813         };
815         /**
816          * Handle the event that triggers sending a message from the messages area.
817          *
818          * @param {event} e The jquery event
819          * @param {object} data Additional event data
820          * @private
821          */
822         Messages.prototype._sendMessageHandler = function(e, data) {
823             data.originalEvent.preventDefault();
825             this._sendMessage();
826         };
828         /**
829          * Hide the messaging area. This only applies on smaller screen resolutions.
830          *
831          * @private
832          */
833         Messages.prototype._hideMessagingArea = function() {
834             this.messageArea.find(SELECTORS.MESSAGINGAREA)
835                 .removeClass('show-messages')
836                 .addClass('hide-messages');
837         };
839         /**
840          * Checks if a day separator needs to be removed.
841          *
842          * Example - scrolling up and loading previous messages that belong to the
843          * same day as the last message that was previously shown, meaning we can
844          * remove the original separator.
845          *
846          * @param {jQuery} domHtml The HTML in the DOM.
847          * @param {jQuery} newHtml The HTML to compare to the DOM
848          * @param {boolean} loadingPreviousMessages Are we loading previous messages?
849          * @return {boolean}
850          * @private
851          */
852         Messages.prototype._hasMatchingBlockTime = function(domHtml, newHtml, loadingPreviousMessages) {
853             var blockTime, blockTimePos, newBlockTime, newBlockTimePos;
855             if (loadingPreviousMessages) {
856                 blockTimePos = ':first';
857                 newBlockTimePos = ':last';
858             } else {
859                 blockTimePos = ':last';
860                 newBlockTimePos = ':first';
861             }
863             blockTime = domHtml.find(SELECTORS.BLOCKTIME + blockTimePos);
864             newBlockTime = newHtml.find(SELECTORS.BLOCKTIME + newBlockTimePos);
866             if (blockTime.length && newBlockTime.length) {
867                 return blockTime.data('blocktime') == newBlockTime.data('blocktime');
868             }
870             return false;
871         };
873         return Messages;
874     }
875 );