MDL-54687 core_message: remove contact from DOM if no messages remain
[moodle.git] / message / amd / src / message_area_messages.js
CommitLineData
e237d2bd
MN
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 * This module handles the message area of the messaging area.
18 *
6b2657d9 19 * @module core_message/message_area_messages
e237d2bd
MN
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 */
4d0fa501
RW
24define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/custom_interaction_events',
25 'core/auto_rows', 'core_message/message_area_actions'],
26 function($, ajax, templates, notification, customEvents, AutoRows, Actions) {
e237d2bd
MN
27
28 /**
29 * Messages class.
30 *
31 * @param {Messagearea} messageArea The messaging area object.
32 */
33 function Messages(messageArea) {
34 this.messageArea = messageArea;
35 this._init();
36 }
37
8ec78c48
MN
38 /** @type {Boolean} checks if we are currently loading messages */
39 Messages.prototype._isLoadingMessages = false;
40
41 /** @type {int} the number of messagess displayed */
42 Messages.prototype._numMessagesDisplayed = 0;
43
44 /** @type {int} the number of messages to retrieve */
45 Messages.prototype._numMessagesToRetrieve = 20;
46
e237d2bd
MN
47 /** @type {Messagearea} The messaging area object. */
48 Messages.prototype.messageArea = null;
49
50 /**
51 * Initialise the event listeners.
52 *
53 * @private
54 */
55 Messages.prototype._init = function() {
fbdcd499
RW
56 customEvents.define(this.messageArea.node, [
57 customEvents.events.activate,
58 customEvents.events.up,
59 customEvents.events.down,
60 ]);
61
4d0fa501
RW
62 AutoRows.init(this.messageArea.node);
63
2a4bbaa3 64 this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONVERSATIONDELETED, this._handleConversationDeleted.bind(this));
8ec78c48
MN
65 this.messageArea.onCustomEvent(this.messageArea.EVENTS.CONVERSATIONSELECTED, this._viewMessages.bind(this));
66 this.messageArea.onCustomEvent(this.messageArea.EVENTS.SENDMESSAGE, this._viewMessages.bind(this));
2a4bbaa3 67 this.messageArea.onCustomEvent(this.messageArea.EVENTS.CHOOSEMESSAGESTODELETE, this._chooseMessagesToDelete.bind(this));
2be15a66
MN
68 this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.SENDMESSAGE,
69 this._sendMessage.bind(this));
70 this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.STARTDELETEMESSAGES,
71 this._startDeleting.bind(this));
72 this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.DELETEMESSAGES,
73 this._deleteMessages.bind(this));
fbdcd499 74 this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.CANCELDELETEMESSAGES,
e237d2bd 75 this._cancelMessagesToDelete.bind(this));
2be15a66
MN
76 this.messageArea.onDelegateEvent(customEvents.events.activate, this.messageArea.SELECTORS.MESSAGE,
77 this._toggleMessage.bind(this));
fbdcd499 78
2be15a66
MN
79 this.messageArea.onDelegateEvent(customEvents.events.up, this.messageArea.SELECTORS.MESSAGE,
80 this._selectPreviousMessage.bind(this));
81 this.messageArea.onDelegateEvent(customEvents.events.down, this.messageArea.SELECTORS.MESSAGE,
82 this._selectNextMessage.bind(this));
4d0fa501
RW
83
84 this.messageArea.onDelegateEvent('focus', this.messageArea.SELECTORS.SENDMESSAGETEXT, this._setMessaging.bind(this));
85 this.messageArea.onDelegateEvent('blur', this.messageArea.SELECTORS.SENDMESSAGETEXT, this._clearMessaging.bind(this));
e237d2bd
MN
86 };
87
88 /**
8ec78c48 89 * View the message panel.
e237d2bd
MN
90 *
91 * @param {Event} event
92 * @param {int} userid
93 * @returns {Promise} The promise resolved when the messages have been loaded.
94 * @private
95 */
8ec78c48
MN
96 Messages.prototype._viewMessages = function(event, userid) {
97 // We are viewing another user, or re-loading the panel, so set number of messages displayed to 0.
98 this._numMessagesDisplayed = 0;
99
100 // Keep track of the number of messages received.
101 var numberreceived = 0;
e237d2bd 102 // Show loading template.
8ec78c48 103 return templates.render('core/loading', {}).then(function(html, js) {
2a4bbaa3 104 templates.replaceNodeContents(this.messageArea.SELECTORS.MESSAGESAREA, html, js);
8ec78c48
MN
105 return this._getMessages(userid);
106 }.bind(this)).then(function(data) {
107 numberreceived = data.messages.length;
108 // We have the data - lets render the template with it.
109 return templates.render('core_message/message_area_messages_area', data);
110 }).then(function(html, js) {
111 templates.replaceNodeContents(this.messageArea.SELECTORS.MESSAGESAREA, html, js);
112 // Scroll to the bottom.
113 this._scrollBottom();
114 // Only increment if data was returned.
115 if (numberreceived > 0) {
116 // Set the number of messages displayed.
117 this._numMessagesDisplayed = numberreceived;
118 }
119 // Now enable the ability to infinitely scroll through messages.
120 customEvents.define(this.messageArea.SELECTORS.MESSAGES, [
121 customEvents.events.scrollTop
122 ]);
123 // Assign the event for scrolling.
124 this.messageArea.onCustomEvent(customEvents.events.scrollTop, this._loadMessages.bind(this));
125 }.bind(this)).fail(notification.exception);
126 };
127
128 /**
129 * Loads messages while scrolling.
130 *
131 * @returns {Promise} The promise resolved when the messages have been loaded.
132 * @private
133 */
134 Messages.prototype._loadMessages = function() {
135 if (this._isLoadingMessages) {
136 return;
137 }
138
139 this._isLoadingMessages = true;
140
141 // Keep track of the number of messages received.
142 var numberreceived = 0;
143 // Show loading template.
144 return templates.render('core/loading', {}).then(function(html, js) {
145 templates.prependNodeContents(this.messageArea.SELECTORS.MESSAGES,
146 "<div style='text-align:center'>" + html + "</div>", js);
147 return this._getMessages(this._getUserId());
148 }.bind(this)).then(function(data) {
149 numberreceived = data.messages.length;
150 // We have the data - lets render the template with it.
151 return templates.render('core_message/message_area_messages', data);
152 }).then(function(html, js) {
153 // Remove the loading icon.
154 this.messageArea.find(this.messageArea.SELECTORS.MESSAGES + " " +
155 this.messageArea.SELECTORS.LOADINGICON).remove();
156 // Check if we got something to do.
157 if (numberreceived > 0) {
158 // Let's check if we can remove the block time.
159 // First, get the block time that is currently being displayed.
160 var blocktime = this.messageArea.node.find(this.messageArea.SELECTORS.BLOCKTIME + ":first");
161 var newblocktime = $(html).find(this.messageArea.SELECTORS.BLOCKTIME + ":first").addBack();
162 if (blocktime.html() == newblocktime.html()) {
163 // Remove the block time as it's present above.
164 blocktime.remove();
165 }
166 // Get height before we add the messages.
167 var oldheight = this.messageArea.find(this.messageArea.SELECTORS.MESSAGES)[0].scrollHeight;
168 // Show the new content.
169 templates.prependNodeContents(this.messageArea.SELECTORS.MESSAGES, html, js);
170 // Get height after we add the messages.
171 var newheight = this.messageArea.find(this.messageArea.SELECTORS.MESSAGES)[0].scrollHeight;
172 // Make sure scroll bar is at the location before we loaded more messages.
173 this.messageArea.find(this.messageArea.SELECTORS.MESSAGES).scrollTop(newheight - oldheight);
174 // Increment the number of messages displayed.
175 this._numMessagesDisplayed += numberreceived;
176 }
177 // Mark that we are no longer busy loading data.
178 this._isLoadingMessages = false;
179 }.bind(this)).fail(notification.exception);
180 };
e237d2bd 181
8ec78c48
MN
182 /**
183 * Handles returning a list of messages to display.
184 *
185 * @param {int} userid
186 * @returns {Promise} The promise resolved when the contact area has been rendered
187 * @private
188 */
189 Messages.prototype._getMessages = function(userid) {
e237d2bd
MN
190 // Call the web service to get our data.
191 var promises = ajax.call([{
192 methodname: 'core_message_data_for_messagearea_messages',
193 args: {
194 currentuserid: this.messageArea.getCurrentUserId(),
8ec78c48
MN
195 otheruserid: userid,
196 limitfrom: this._numMessagesDisplayed,
197 limitnum: this._numMessagesToRetrieve,
198 newest: true
e237d2bd
MN
199 }
200 }]);
201
202 // Do stuff when we get data back.
8ec78c48 203 return promises[0];
e237d2bd
MN
204 };
205
206 /**
207 * Handles sending a message.
208 *
209 * @returns {Promise} The promise resolved once the message has been sent.
210 * @private
211 */
212 Messages.prototype._sendMessage = function() {
9661810e 213 var text = this.messageArea.find(this.messageArea.SELECTORS.SENDMESSAGETEXT).val();
2c1b3775
MN
214
215 // Do not do anything if it is empty.
216 if (text.trim() === '') {
217 return;
218 }
219
e237d2bd
MN
220 // Call the web service to save our message.
221 var promises = ajax.call([{
222 methodname: 'core_message_send_instant_messages',
223 args: {
224 messages: [
225 {
226 touserid: this._getUserId(),
9661810e 227 text: text
e237d2bd
MN
228 }
229 ]
230 }
231 }]);
232
233 // Update the DOM when we get some data back.
234 return promises[0].then(function() {
e237d2bd 235 // Fire an event to say the message was sent.
9661810e 236 this.messageArea.trigger(this.messageArea.EVENTS.MESSAGESENT, [this._getUserId(), text]);
e237d2bd
MN
237 // Update the messaging area.
238 this._addMessageToDom();
239 }.bind(this)).fail(notification.exception);
240 };
241
242 /**
243 * Handles selecting messages to delete.
244 *
245 * @returns {Promise} The promise resolved when the messages to delete have been selected.
246 * @private
247 */
248 Messages.prototype._chooseMessagesToDelete = function() {
4d0fa501
RW
249 this.messageArea.find(this.messageArea.SELECTORS.MESSAGESAREA).addClass('editing');
250 this.messageArea.find(this.messageArea.SELECTORS.MESSAGE)
251 .attr('role', 'checkbox')
252 .attr('aria-checked', 'false');
e237d2bd
MN
253 };
254
255 /**
256 * Handles deleting messages.
257 *
258 * @private
259 */
260 Messages.prototype._deleteMessages = function() {
261 var userid = this.messageArea.getCurrentUserId();
4d0fa501 262 var checkboxes = this.messageArea.find(this.messageArea.SELECTORS.MESSAGE + "[aria-checked='true']");
e237d2bd
MN
263 var requests = [];
264 var messagestoremove = [];
265
266 // Go through all the checked checkboxes and prepare them for deletion.
267 checkboxes.each(function(id, element) {
268 var node = $(element);
269 var messageid = node.data('messageid');
270 var isread = node.data('messageread') ? 1 : 0;
4d0fa501 271 messagestoremove.push(node);
e237d2bd
MN
272 requests.push({
273 methodname: 'core_message_delete_message',
274 args: {
275 messageid: messageid,
276 userid: userid,
277 read: isread
278 }
279 });
280 }.bind(this));
281
282 if (requests.length > 0) {
283 ajax.call(requests)[requests.length - 1].then(function() {
284 // Remove the messages from the DOM.
285 $.each(messagestoremove, function(key, message) {
286 // Remove the message.
287 message.remove();
288 });
289 // Now we have removed all the messages from the DOM lets remove any block times we may need to as well.
290 $.each(messagestoremove, function(key, message) {
291 // First - let's make sure there are no more messages in that time block.
292 var blocktime = message.data('blocktime');
2a4bbaa3
MN
293 if (this.messageArea.find(this.messageArea.SELECTORS.MESSAGE +
294 "[data-blocktime='" + blocktime + "']").length === 0) {
295 this.messageArea.find(this.messageArea.SELECTORS.BLOCKTIME +
296 "[data-blocktime='" + blocktime + "']").remove();
e237d2bd
MN
297 }
298 }.bind(this));
9661810e 299
d29cdf3a
MN
300 // If there are no messages at all, then remove conversation panel.
301 if (this.messageArea.find(this.messageArea.SELECTORS.MESSAGE).length === 0) {
302 this.messageArea.find(this.messageArea.SELECTORS.CONVERSATIONS + " " +
303 this.messageArea.SELECTORS.CONTACT + "[data-userid='" + this._getUserId() + "']").remove();
304 }
305
9661810e
MN
306 // Trigger event letting other modules know messages were deleted.
307 this.messageArea.trigger(this.messageArea.EVENTS.MESSAGESDELETED, this._getUserId());
e237d2bd 308 }.bind(this), notification.exception);
9661810e
MN
309 } else {
310 // Trigger event letting other modules know messages were deleted.
311 this.messageArea.trigger(this.messageArea.EVENTS.MESSAGESDELETED, this._getUserId());
e237d2bd 312 }
dec0cd99
MN
313
314 // Hide the items responsible for deleting messages.
315 this._hideDeleteAction();
316
9661810e 317
e237d2bd
MN
318 };
319
320 /**
dec0cd99 321 * Returns the ID of the other user in the conversation.
e237d2bd 322 *
dec0cd99
MN
323 * @params {Event} event
324 * @params {int} The user id
e237d2bd
MN
325 * @private
326 */
dec0cd99
MN
327 Messages.prototype._handleConversationDeleted = function(event, userid) {
328 if (userid == this._getUserId()) {
329 // Clear the current panel.
2a4bbaa3 330 this.messageArea.find(this.messageArea.SELECTORS.MESSAGESAREA).empty();
dec0cd99
MN
331 }
332 };
333
334 /**
335 * Handles hiding the delete checkboxes and replacing the response area.
336 *
337 * @return {Promise} JQuery promise object resolved when the template has been rendered.
338 * @private
339 */
340 Messages.prototype._hideDeleteAction = function() {
4d0fa501
RW
341 this.messageArea.find(this.messageArea.SELECTORS.MESSAGE)
342 .removeAttr('role')
343 .removeAttr('aria-checked');
344 this.messageArea.find(this.messageArea.SELECTORS.MESSAGESAREA).removeClass('editing');
e237d2bd
MN
345 };
346
dec0cd99
MN
347 /**
348 * Handles canceling deleting messages.
349 *
350 * @private
351 */
352 Messages.prototype._cancelMessagesToDelete = function() {
353 // Hide the items responsible for deleting messages.
354 this._hideDeleteAction();
355 // Trigger event letting other modules know message deletion was canceled.
2a4bbaa3 356 this.messageArea.trigger(this.messageArea.EVENTS.CANCELDELETEMESSAGES);
dec0cd99
MN
357 };
358
e237d2bd
MN
359 /**
360 * Handles adding messages to the DOM.
361 *
362 * @returns {Promise} The promise resolved when the message has been added to the DOM.
363 * @private
364 */
365 Messages.prototype._addMessageToDom = function() {
366 // Call the web service to return how the message should look.
367 var promises = ajax.call([{
368 methodname: 'core_message_data_for_messagearea_get_most_recent_message',
369 args: {
370 currentuserid: this.messageArea.getCurrentUserId(),
371 otheruserid: this._getUserId()
372 }
373 }]);
374
375 // Add the message.
376 return promises[0].then(function(data) {
6b2657d9 377 return templates.render('core_message/message_area_message', data);
e237d2bd 378 }).then(function(html, js) {
2a4bbaa3 379 templates.appendNodeContents(this.messageArea.SELECTORS.MESSAGES, html, js);
e237d2bd 380 // Empty the response text area.
4d0fa501 381 this.messageArea.find(this.messageArea.SELECTORS.SENDMESSAGETEXT).val('').trigger('input');
8ec78c48
MN
382 // Scroll down.
383 this._scrollBottom();
e237d2bd
MN
384 }.bind(this)).fail(notification.exception);
385 };
386
387 /**
388 * Returns the ID of the other user in the conversation.
389 *
390 * @returns {int} The user id
391 * @private
392 */
393 Messages.prototype._getUserId = function() {
2a4bbaa3 394 return this.messageArea.find(this.messageArea.SELECTORS.MESSAGES).data('userid');
e237d2bd
MN
395 };
396
8ec78c48
MN
397 /**
398 * Scrolls to the bottom of the messages.
399 *
400 * @private
401 */
402 Messages.prototype._scrollBottom = function() {
403 // Scroll to the bottom.
404 var messages = this.messageArea.find(this.messageArea.SELECTORS.MESSAGES);
405 messages.scrollTop(messages[0].scrollHeight);
406 };
407
fbdcd499
RW
408 /**
409 * Select the previous message in the list.
410 *
411 * @params {event} e The jquery event
412 * @params {object} data Extra event data
413 * @private
414 */
415 Messages.prototype._selectPreviousMessage = function(e, data) {
416 var currentMessage = $(e.target).closest(this.messageArea.SELECTORS.MESSAGE);
417
418 do {
419 currentMessage = currentMessage.prev();
420 } while (currentMessage.length && !currentMessage.is(this.messageArea.SELECTORS.MESSAGE));
421
422 currentMessage.focus();
423
424 data.originalEvent.preventDefault();
425 data.originalEvent.stopPropagation();
426 };
427
428 /**
429 * Select the next message in the list.
430 *
431 * @params {event} e The jquery event
432 * @params {object} data Extra event data
433 * @private
434 */
435 Messages.prototype._selectNextMessage = function(e, data) {
436 var currentMessage = $(e.target).closest(this.messageArea.SELECTORS.MESSAGE);
437
438 do {
439 currentMessage = currentMessage.next();
440 } while (currentMessage.length && !currentMessage.is(this.messageArea.SELECTORS.MESSAGE));
441
442 currentMessage.focus();
443
444 data.originalEvent.preventDefault();
445 data.originalEvent.stopPropagation();
446 };
447
4d0fa501
RW
448 /**
449 * Flag the response area as messaging.
450 *
451 * @params {event} e The jquery event
452 * @private
453 */
454 Messages.prototype._setMessaging = function(e) {
455 $(e.target).closest(this.messageArea.SELECTORS.MESSAGERESPONSE).addClass('messaging');
456 };
457
458 /**
459 * Clear the response area as messaging flag.
460 *
461 * @params {event} e The jquery event
462 * @private
463 */
464 Messages.prototype._clearMessaging = function(e) {
465 $(e.target).closest(this.messageArea.SELECTORS.MESSAGERESPONSE).removeClass('messaging');
466 };
467
468 /**
469 * Turn on delete message mode.
470 *
471 * @params {event} e The jquery event
472 * @private
473 */
474 Messages.prototype._startDeleting = function(e) {
475 var actions = new Actions(this.messageArea);
476 actions.chooseMessagesToDelete();
477
478 e.preventDefault();
479 };
480
481 /**
482 * Check if the message area is in editing mode.
483 *
484 * @return {bool}
485 * @private
486 */
487 Messages.prototype._isEditing = function() {
488 return this.messageArea.find(this.messageArea.SELECTORS.MESSAGESAREA).hasClass('editing');
489 };
490
491 /**
492 * Check or uncheck the message if the message area is in editing mode.
493 *
494 * @params {event} e The jquery event
4d0fa501
RW
495 * @private
496 */
2be15a66 497 Messages.prototype._toggleMessage = function(e) {
4d0fa501
RW
498 if (!this._isEditing()) {
499 return;
500 }
501
502 var message = $(e.target).closest(this.messageArea.SELECTORS.MESSAGE);
503
504 if (message.attr('aria-checked') === 'true') {
505 message.attr('aria-checked', 'false');
506 } else {
507 message.attr('aria-checked', 'true');
508 }
509 };
510
e237d2bd
MN
511 return Messages;
512 }
fbdcd499 513);