From fb1469d84f409becbd8b24cedaa5b936622052f6 Mon Sep 17 00:00:00 2001 From: Ryan Wyllie Date: Thu, 20 Oct 2016 06:01:57 +0000 Subject: [PATCH 1/3] MDL-56139 message: ajax poll for new messages in message area --- lang/en/cache.php | 1 + lib/amd/build/backoff_timer.min.js | 1 + lib/amd/src/backoff_timer.js | 206 ++++++++++++++++++ lib/db/caches.php | 9 + lib/messagelib.php | 10 + .../amd/build/message_area_messages.min.js | 2 +- message/amd/src/message_area_messages.js | 145 +++++++++++- message/classes/api.php | 25 ++- message/classes/helper.php | 31 ++- .../message_last_created_cache_source.php | 89 ++++++++ .../classes/output/messagearea/message.php | 1 + message/externallib.php | 32 ++- message/tests/api_test.php | 125 +++++++++++ message/tests/externallib_test.php | 40 ++++ version.php | 4 +- 15 files changed, 697 insertions(+), 24 deletions(-) create mode 100644 lib/amd/build/backoff_timer.min.js create mode 100644 lib/amd/src/backoff_timer.js create mode 100644 message/classes/message_last_created_cache_source.php diff --git a/lang/en/cache.php b/lang/en/cache.php index 99e33102e7d33..ae490cbaadb1e 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -52,6 +52,7 @@ $string['cachedef_groupdata'] = 'Course group information'; $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content'; $string['cachedef_langmenu'] = 'List of available languages'; +$string['cachedef_message_last_created'] = 'Time created for most recent message between users'; $string['cachedef_locking'] = 'Locking'; $string['cachedef_message_processors_enabled'] = "Message processors enabled status"; $string['cachedef_navigation_expandcourse'] = 'Navigation expandable courses'; diff --git a/lib/amd/build/backoff_timer.min.js b/lib/amd/build/backoff_timer.min.js new file mode 100644 index 0000000000000..b1d81162ea102 --- /dev/null +++ b/lib/amd/build/backoff_timer.min.js @@ -0,0 +1 @@ +define(function(){var a=1e3,b=function(b,c){if(!b)return a;if(c.length){var d=c[c.length-1];return b+d}return a},c=function(a){this.reset(),this.setCallback(a),this.setBackOffFunction(b)};return c.prototype.setCallback=function(a){return this.callback=a,this},c.prototype.getCallback=function(){return this.callback},c.prototype.setBackOffFunction=function(a){return this.backOffFunction=a,this},c.prototype.getBackOffFunction=function(){return this.backOffFunction},c.prototype.generateNextTime=function(){var a=this.getBackOffFunction().call(this.getBackOffFunction(),this.time,this.previousTimes);return this.previousTimes.push(this.time),this.time=a,a},c.prototype.reset=function(){return this.time=null,this.previousTimes=[],this.stop(),this},c.prototype.stop=function(){return this.timeout&&(window.clearTimeout(this.timeout),this.timeout=null),this},c.prototype.start=function(){if(!this.timeout){var a=this.generateNextTime();this.timeout=window.setTimeout(function(){this.getCallback().call(),this.stop(),this.start()}.bind(this),a)}return this},c.prototype.restart=function(){return this.reset().start()},c}); \ No newline at end of file diff --git a/lib/amd/src/backoff_timer.js b/lib/amd/src/backoff_timer.js new file mode 100644 index 0000000000000..992a322d70660 --- /dev/null +++ b/lib/amd/src/backoff_timer.js @@ -0,0 +1,206 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * A timer that will execute a callback with decreasing frequency. Useful for + * doing polling on the server without overwhelming it with requests. + * + * @module core/backoff_timer + * @class backoff_timer + * @package core + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +define(function() { + + // Default to one second. + var DEFAULT_TIME = 1000; + + /** + * The default back off function for the timer. It uses the Fibonacci + * sequence to determine what the next timeout value should be. + * + * @param {(int|null)} time The current timeout value or null if none set + * @param {array} previousTimes An array containing all previous timeout values + * @return {int} The new timeout value + */ + var fibonacciBackOff = function(time, previousTimes) { + if (!time) { + return DEFAULT_TIME; + } + + if (previousTimes.length) { + var lastTime = previousTimes[previousTimes.length - 1]; + return time + lastTime; + } else { + return DEFAULT_TIME; + } + }; + + /** + * Constructor for the back off timer. + * + * @param {function} callback The function to execute after each tick + */ + var Timer = function(callback) { + this.reset(); + this.setCallback(callback); + // Set the default backoff function to be the Fibonacci sequence. + this.setBackOffFunction(fibonacciBackOff); + }; + + /** + * Set the callback function to be executed after each tick of the + * timer. + * + * @method setCallback + * @param {function} callback The callback function + * @return {object} this + */ + Timer.prototype.setCallback = function(callback) { + this.callback = callback; + + return this; + }; + + /** + * Get the callback function for this timer. + * + * @method getCallback + * @return {function} + */ + Timer.prototype.getCallback = function() { + return this.callback; + }; + + /** + * Set the function to be used when calculating the back off time + * for each tick of the timer. + * + * The back off function will be given two parameters: the current + * time and an array containing all previous times. + * + * @method setBackOffFunction + * @param {function} backOffFunction The function to calculate back off times + * @return {object} this + */ + Timer.prototype.setBackOffFunction = function(backOffFunction) { + this.backOffFunction = backOffFunction; + + return this; + }; + + /** + * Get the current back off function. + * + * @method getBackOffFunction + * @return {function} + */ + Timer.prototype.getBackOffFunction = function() { + return this.backOffFunction; + }; + + /** + * Generate the next timeout in the back off time sequence + * for the timer. + * + * The back off function is called to calculate the next value. + * It is given the current value and an array of all previous values. + * + * @method generateNextTime + * @return {int} The new timeout value (in milliseconds) + */ + Timer.prototype.generateNextTime = function() { + var newTime = this.getBackOffFunction().call( + this.getBackOffFunction(), + this.time, + this.previousTimes + ); + this.previousTimes.push(this.time); + this.time = newTime; + + return newTime; + }; + + /** + * Stop the current timer and clear the previous time values + * + * @method reset + * @return {object} this + */ + Timer.prototype.reset = function() { + this.time = null; + this.previousTimes = []; + this.stop(); + + return this; + }; + + /** + * Clear the current timeout, if one is set. + * + * @method stop + * @return {object} this + */ + Timer.prototype.stop = function() { + if (this.timeout) { + window.clearTimeout(this.timeout); + this.timeout = null; + } + + return this; + }; + + /** + * Start the current timer by generating the new timeout value and + * starting the ticks. + * + * This function recurses after each tick with a new timeout value + * generated each time. + * + * The callback function is called after each tick. + * + * @method start + * @return {object} this + */ + Timer.prototype.start = function() { + // If we haven't already started. + if (!this.timeout) { + var time = this.generateNextTime(); + this.timeout = window.setTimeout(function() { + this.getCallback().call(); + // Clear the existing timer. + this.stop(); + // Start the next timer. + this.start(); + }.bind(this), time); + } + + return this; + }; + + /** + * Reset the timer and start it again from the initial timeout + * values + * + * @method restart + * @return {object} this + */ + Timer.prototype.restart = function() { + return this.reset().start(); + }; + + return Timer; +}); diff --git a/lib/db/caches.php b/lib/db/caches.php index b709f06f0615d..49e509a818654 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -301,4 +301,13 @@ 'staticacceleration' => true, 'staticaccelerationsize' => 3 ), + + // Cache for storing the user's last received message time. + 'message_last_created' => array( + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, // The id of the sender and recipient is used. + 'simplevalues' => true, + 'datasource' => 'message_last_created_cache_source', + 'datasourcefile' => 'message/classes/message_last_created_cache_source.php' + ), ); diff --git a/lib/messagelib.php b/lib/messagelib.php index 34e7c38ae927a..9bfad534db6f4 100644 --- a/lib/messagelib.php +++ b/lib/messagelib.php @@ -234,6 +234,16 @@ function message_send($eventdata) { } } + // Only cache messages, not notifications. + if (empty($savemessage->notification)) { + // Cache the timecreated value of the last message between these two users. + $cache = cache::make('core', 'message_last_created'); + $ids = [$savemessage->useridfrom, $savemessage->useridto]; + sort($ids); + $key = implode('_', $ids); + $cache->set($key, $savemessage->timecreated); + } + // Store unread message just in case we get a fatal error any time later. $savemessage->id = $DB->insert_record('message', $savemessage); $eventdata->savedmessageid = $savemessage->id; diff --git a/message/amd/build/message_area_messages.min.js b/message/amd/build/message_area_messages.min.js index 79d87c85fd429..4ba61b0677a40 100644 --- a/message/amd/build/message_area_messages.min.js +++ b/message/amd/build/message_area_messages.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/templates","core/notification","core/custom_interaction_events","core/auto_rows","core_message/message_area_actions","core/modal_factory","core/modal_events","core/str","core_message/message_area_events"],function(a,b,c,d,e,f,g,h,i,j,k){function l(a){this.messageArea=a,this._init()}var m=500,n=50,o={BLOCKTIME:"[data-region='blocktime']",CANCELDELETEMESSAGES:"[data-action='cancel-delete-messages']",CONTACT:"[data-region='contact']",CONVERSATIONS:"[data-region='contacts'][data-region-content='conversations']",DELETEALLMESSAGES:"[data-action='delete-all-messages']",DELETEMESSAGES:"[data-action='delete-messages']",LOADINGICON:".loading-icon",MESSAGE:"[data-region='message']",MESSAGERESPONSE:"[data-region='response']",MESSAGES:"[data-region='messages']",MESSAGESAREA:"[data-region='messages-area']",MESSAGINGAREA:"[data-region='messaging-area']",SENDMESSAGE:"[data-action='send-message']",SENDMESSAGETEXT:"[data-region='send-message-txt']",SHOWCONTACTS:"[data-action='show-contacts']",STARTDELETEMESSAGES:"[data-action='start-delete-messages']"};return l.prototype._isSendingMessage=!1,l.prototype._isLoadingMessages=!1,l.prototype._numMessagesDisplayed=0,l.prototype._numMessagesToRetrieve=20,l.prototype._confirmationModal=null,l.prototype.messageArea=null,l.prototype._init=function(){e.define(this.messageArea.node,[e.events.activate,e.events.up,e.events.down,e.events.enter]),a(window).height()<=670&&(m=400),f.init(this.messageArea.node),this.messageArea.onCustomEvent(k.CONVERSATIONSELECTED,this._viewMessages.bind(this)),this.messageArea.onCustomEvent(k.SENDMESSAGE,this._viewMessages.bind(this)),this.messageArea.onCustomEvent(k.CHOOSEMESSAGESTODELETE,this._chooseMessagesToDelete.bind(this)),this.messageArea.onCustomEvent(k.CANCELDELETEMESSAGES,this._hideDeleteAction.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,o.SENDMESSAGE,this._sendMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,o.STARTDELETEMESSAGES,this._startDeleting.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,o.DELETEMESSAGES,this._deleteMessages.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,o.DELETEALLMESSAGES,this._deleteAllMessages.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,o.CANCELDELETEMESSAGES,this._triggerCancelMessagesToDelete.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,o.MESSAGE,this._toggleMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,o.SHOWCONTACTS,this._hideMessagingArea.bind(this)),this.messageArea.onDelegateEvent(e.events.up,o.MESSAGE,this._selectPreviousMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.down,o.MESSAGE,this._selectNextMessage.bind(this)),this.messageArea.onDelegateEvent("focus",o.SENDMESSAGETEXT,this._setMessaging.bind(this)),this.messageArea.onDelegateEvent("blur",o.SENDMESSAGETEXT,this._clearMessaging.bind(this)),this.messageArea.onDelegateEvent(e.events.enter,o.SENDMESSAGETEXT,this._sendMessageHandler.bind(this)),a(document).on(f.events.ROW_CHANGE,this._adjustMessagesAreaHeight.bind(this));var b=this.messageArea.find(o.MESSAGES);b.length&&this._addScrollEventListener(b.find(o.MESSAGE).length)},l.prototype._viewMessages=function(e,f){this._numMessagesDisplayed=0;var g=b.call([{methodname:"core_message_mark_all_messages_as_read",args:{useridto:this.messageArea.getCurrentUserId(),useridfrom:f}}]),h=0;return c.render("core/loading",{}).then(function(a,b){return c.replaceNodeContents(this.messageArea.find(o.MESSAGESAREA),a,b),g[0]}.bind(this)).then(function(){var b=this.messageArea.find(o.CONVERSATIONS+" "+o.CONTACT+"[data-userid='"+f+"']");return b.hasClass("unread")&&(b.removeClass("unread"),a(document).trigger("messagearea:conversationselected",f)),this._getMessages(f)}.bind(this)).then(function(a){return h=a.messages.length,c.render("core_message/message_area_messages_area",a)}).then(function(a,b){c.replaceNodeContents(this.messageArea.find(o.MESSAGESAREA),a,b),this._addScrollEventListener(h)}.bind(this)).fail(d.exception)},l.prototype._loadMessages=function(){if(this._isLoadingMessages)return!1;this._isLoadingMessages=!0;var b=0;return c.render("core/loading",{}).then(function(a,b){return c.prependNodeContents(this.messageArea.find(o.MESSAGES),"
"+a+"
",b),this._getMessages(this._getUserId())}.bind(this)).then(function(a){return b=a.messages.length,c.render("core_message/message_area_messages",a)}).then(function(d,e){if(this.messageArea.find(o.MESSAGES+" "+o.LOADINGICON).remove(),b>0){var f=this.messageArea.node.find(o.BLOCKTIME+":first"),g=a(d).find(o.BLOCKTIME+":first").addBack();f.html()==g.html()&&f.remove();var h=this.messageArea.find(o.MESSAGES)[0].scrollHeight;c.prependNodeContents(this.messageArea.find(o.MESSAGES),d,e);var i=this.messageArea.find(o.MESSAGES)[0].scrollHeight;this.messageArea.find(o.MESSAGES).scrollTop(i-h),this._numMessagesDisplayed+=b}this._isLoadingMessages=!1}.bind(this)).fail(d.exception)},l.prototype._getMessages=function(a){var c=b.call([{methodname:"core_message_data_for_messagearea_messages",args:{currentuserid:this.messageArea.getCurrentUserId(),otheruserid:a,limitfrom:this._numMessagesDisplayed,limitnum:this._numMessagesToRetrieve,newest:!0}}]);return c[0]},l.prototype._sendMessage=function(){var a=this.messageArea.find(o.SENDMESSAGETEXT),c=a.val();if(""===c.trim())return!1;if(this._isSendingMessage)return!1;this._isSendingMessage=!0;var e=b.call([{methodname:"core_message_send_instant_messages",args:{messages:[{touserid:this._getUserId(),text:c}]}}]);return a.prop("disabled",!0),e[0].then(function(a){if(a.length<0)throw new Error("Invalid response");if(a[0].errormessage)throw new Error(a[0].errormessage);return this.messageArea.trigger(k.MESSAGESENT,[this._getUserId(),c]),this._addMessageToDom()}.bind(this)).then(function(){this._isSendingMessage=!1}.bind(this)).always(function(){a.prop("disabled",!1)}).fail(d.exception)},l.prototype._chooseMessagesToDelete=function(){this.messageArea.find(o.MESSAGESAREA).addClass("editing"),this.messageArea.find(o.MESSAGE).attr("role","checkbox").attr("aria-checked","false")},l.prototype._deleteMessages=function(){var c=this.messageArea.getCurrentUserId(),e=this.messageArea.find(o.MESSAGE+"[aria-checked='true']"),f=[],g=[];e.each(function(b,d){var e=a(d),h=e.data("messageid"),i=e.data("messageread")?1:0;g.push(e),f.push({methodname:"core_message_delete_message",args:{messageid:h,userid:c,read:i}})}),f.length>0?b.call(f)[f.length-1].then(function(){var b=null,c=this.messageArea.find(o.MESSAGE),d=c.last(),e=g[g.length-1];a.each(g,function(a,b){b.remove()}),d.data("id")===e.data("id")&&(b=this.messageArea.find(o.MESSAGE).last()),a.each(g,function(a,b){var c=b.data("blocktime");0===this.messageArea.find(o.MESSAGE+"[data-blocktime='"+c+"']").length&&this.messageArea.find(o.BLOCKTIME+"[data-blocktime='"+c+"']").remove()}.bind(this)),0===this.messageArea.find(o.MESSAGE).length&&this.messageArea.find(o.CONVERSATIONS+" "+o.CONTACT+"[data-userid='"+this._getUserId()+"']").remove(),this.messageArea.trigger(k.MESSAGESDELETED,[this._getUserId(),b])}.bind(this),d.exception):this.messageArea.trigger(k.MESSAGESDELETED,this._getUserId()),this._hideDeleteAction()},l.prototype._addScrollEventListener=function(a){this._scrollBottom(),this._numMessagesDisplayed=a,e.define(this.messageArea.find(o.MESSAGES),[e.events.scrollTop]),this.messageArea.onCustomEvent(e.events.scrollTop,this._loadMessages.bind(this))},l.prototype._deleteAllMessages=function(){this._confirmationModal?this._confirmationModal.show():j.get_strings([{key:"confirm"},{key:"deleteallconfirm",component:"message"}]).done(function(a){h.create({title:a[0],type:h.types.CONFIRM,body:a[1]},this.messageArea.find(o.DELETEALLMESSAGES)).done(function(a){this._confirmationModal=a,a.getRoot().on(i.yes,function(){var a=this._getUserId(),c={methodname:"core_message_delete_conversation",args:{userid:this.messageArea.getCurrentUserId(),otheruserid:a}};b.call([c])[0].then(function(){this.messageArea.find(o.MESSAGESAREA).empty(),this.messageArea.trigger(k.CONVERSATIONDELETED,a),this._hideDeleteAction()}.bind(this),d.exception)}.bind(this)),a.show()}.bind(this))}.bind(this))},l.prototype._hideDeleteAction=function(){this.messageArea.find(o.MESSAGE).removeAttr("role").removeAttr("aria-checked"),this.messageArea.find(o.MESSAGESAREA).removeClass("editing")},l.prototype._triggerCancelMessagesToDelete=function(){this.messageArea.trigger(k.CANCELDELETEMESSAGES)},l.prototype._addMessageToDom=function(){var a=b.call([{methodname:"core_message_data_for_messagearea_get_most_recent_message",args:{currentuserid:this.messageArea.getCurrentUserId(),otheruserid:this._getUserId()}}]);return a[0].then(function(a){return c.render("core_message/message_area_message",a)}).then(function(a,b){c.appendNodeContents(this.messageArea.find(o.MESSAGES),a,b),this.messageArea.find(o.SENDMESSAGETEXT).val("").trigger("input"),this._scrollBottom()}.bind(this)).fail(d.exception)},l.prototype._getUserId=function(){return this.messageArea.find(o.MESSAGES).data("userid")},l.prototype._scrollBottom=function(){var a=this.messageArea.find(o.MESSAGES);0!==a.length&&a.scrollTop(a[0].scrollHeight)},l.prototype._selectPreviousMessage=function(b,c){var d=a(b.target).closest(o.MESSAGE);do d=d.prev();while(d.length&&!d.is(o.MESSAGE));d.focus(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()},l.prototype._selectNextMessage=function(b,c){var d=a(b.target).closest(o.MESSAGE);do d=d.next();while(d.length&&!d.is(o.MESSAGE));d.focus(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()},l.prototype._setMessaging=function(b){a(b.target).closest(o.MESSAGERESPONSE).addClass("messaging")},l.prototype._clearMessaging=function(b){a(b.target).closest(o.MESSAGERESPONSE).removeClass("messaging")},l.prototype._startDeleting=function(a){var b=new g(this.messageArea);b.chooseMessagesToDelete(),a.preventDefault()},l.prototype._isEditing=function(){return this.messageArea.find(o.MESSAGESAREA).hasClass("editing")},l.prototype._toggleMessage=function(b){if(this._isEditing()){var c=a(b.target).closest(o.MESSAGE);"true"===c.attr("aria-checked")?c.attr("aria-checked","false"):c.attr("aria-checked","true")}},l.prototype._adjustMessagesAreaHeight=function(){var a=this.messageArea.find(o.MESSAGES),b=this.messageArea.find(o.MESSAGERESPONSE),c=b.outerHeight(),d=c-n,e=m-d;a.outerHeight(e)},l.prototype._sendMessageHandler=function(a,b){b.originalEvent.preventDefault(),this._sendMessage()},l.prototype._hideMessagingArea=function(){this.messageArea.find(o.MESSAGINGAREA).removeClass("show-messages").addClass("hide-messages")},l}); \ No newline at end of file +define(["jquery","core/ajax","core/templates","core/notification","core/custom_interaction_events","core/auto_rows","core_message/message_area_actions","core/modal_factory","core/modal_events","core/str","core_message/message_area_events","core/backoff_timer"],function(a,b,c,d,e,f,g,h,i,j,k,l){function m(a){this.messageArea=a,this._init()}var n=500,o=50,p={BLOCKTIME:"[data-region='blocktime']",CANCELDELETEMESSAGES:"[data-action='cancel-delete-messages']",CONTACT:"[data-region='contact']",CONVERSATIONS:"[data-region='contacts'][data-region-content='conversations']",DELETEALLMESSAGES:"[data-action='delete-all-messages']",DELETEMESSAGES:"[data-action='delete-messages']",LOADINGICON:".loading-icon",MESSAGE:"[data-region='message']",MESSAGERESPONSE:"[data-region='response']",MESSAGES:"[data-region='messages']",MESSAGESAREA:"[data-region='messages-area']",MESSAGINGAREA:"[data-region='messaging-area']",SENDMESSAGE:"[data-action='send-message']",SENDMESSAGETEXT:"[data-region='send-message-txt']",SHOWCONTACTS:"[data-action='show-contacts']",STARTDELETEMESSAGES:"[data-action='start-delete-messages']"};return m.prototype._isSendingMessage=!1,m.prototype._isLoadingMessages=!1,m.prototype._numMessagesDisplayed=0,m.prototype._numMessagesToRetrieve=20,m.prototype._confirmationModal=null,m.prototype._earliestMessageTimestamp=0,m.prototype._timer=null,m.prototype.messageArea=null,m.prototype._init=function(){e.define(this.messageArea.node,[e.events.activate,e.events.up,e.events.down,e.events.enter]),a(window).height()<=670&&(n=400),f.init(this.messageArea.node),this.messageArea.onCustomEvent(k.CONVERSATIONSELECTED,this._viewMessages.bind(this)),this.messageArea.onCustomEvent(k.SENDMESSAGE,this._viewMessages.bind(this)),this.messageArea.onCustomEvent(k.CHOOSEMESSAGESTODELETE,this._chooseMessagesToDelete.bind(this)),this.messageArea.onCustomEvent(k.CANCELDELETEMESSAGES,this._hideDeleteAction.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.SENDMESSAGE,this._sendMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.STARTDELETEMESSAGES,this._startDeleting.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.DELETEMESSAGES,this._deleteMessages.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.DELETEALLMESSAGES,this._deleteAllMessages.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.CANCELDELETEMESSAGES,this._triggerCancelMessagesToDelete.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.MESSAGE,this._toggleMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.SHOWCONTACTS,this._hideMessagingArea.bind(this)),this.messageArea.onDelegateEvent(e.events.up,p.MESSAGE,this._selectPreviousMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.down,p.MESSAGE,this._selectNextMessage.bind(this)),this.messageArea.onDelegateEvent("focus",p.SENDMESSAGETEXT,this._setMessaging.bind(this)),this.messageArea.onDelegateEvent("blur",p.SENDMESSAGETEXT,this._clearMessaging.bind(this)),this.messageArea.onDelegateEvent(e.events.enter,p.SENDMESSAGETEXT,this._sendMessageHandler.bind(this)),a(document).on(f.events.ROW_CHANGE,this._adjustMessagesAreaHeight.bind(this));var b=this.messageArea.find(p.MESSAGES);b.length&&this._addScrollEventListener(b.find(p.MESSAGE).length),this._timer=new l(function(){this._loadNewMessages()}.bind(this)),this._timer.start()},m.prototype._viewMessages=function(e,f){this._numMessagesDisplayed=0,this._timer.stop(),this._earliestMessageTimestamp=0;var g=b.call([{methodname:"core_message_mark_all_messages_as_read",args:{useridto:this.messageArea.getCurrentUserId(),useridfrom:f}}]),h=0;return c.render("core/loading",{}).then(function(a,b){return c.replaceNodeContents(this.messageArea.find(p.MESSAGESAREA),a,b),g[0]}.bind(this)).then(function(){var b=this.messageArea.find(p.CONVERSATIONS+" "+p.CONTACT+"[data-userid='"+f+"']");return b.hasClass("unread")&&(b.removeClass("unread"),a(document).trigger("messagearea:conversationselected",f)),this._getMessages(f)}.bind(this)).then(function(a){return h=a.messages.length,c.render("core_message/message_area_messages_area",a)}).then(function(a,b){c.replaceNodeContents(this.messageArea.find(p.MESSAGESAREA),a,b),this._addScrollEventListener(h),this._timer.restart()}.bind(this)).fail(d.exception)},m.prototype._loadMessages=function(){if(this._isLoadingMessages)return!1;this._isLoadingMessages=!0;var b=0;return c.render("core/loading",{}).then(function(a,b){return c.prependNodeContents(this.messageArea.find(p.MESSAGES),"
"+a+"
",b),this._getMessages(this._getUserId())}.bind(this)).then(function(a){return b=a.messages.length,c.render("core_message/message_area_messages",a)}).then(function(d,e){if(this.messageArea.find(p.MESSAGES+" "+p.LOADINGICON).remove(),b>0){var f=this.messageArea.node.find(p.BLOCKTIME+":first"),g=a(d).find(p.BLOCKTIME+":first").addBack();f.html()==g.html()&&f.remove();var h=this.messageArea.find(p.MESSAGES)[0].scrollHeight;c.prependNodeContents(this.messageArea.find(p.MESSAGES),d,e);var i=this.messageArea.find(p.MESSAGES)[0].scrollHeight;this.messageArea.find(p.MESSAGES).scrollTop(i-h),this._numMessagesDisplayed+=b}this._isLoadingMessages=!1}.bind(this)).fail(d.exception)},m.prototype._loadNewMessages=function(){if(this._isLoadingMessages)return!1;if(!this._getUserId())return!1;this._isLoadingMessages=!0;var b=!1,e=this.messageArea.find(p.MESSAGES);if(0!==e.length){var f=e.scrollTop(),g=e.innerHeight(),h=e[0].scrollHeight;f+g>=h&&(b=!0)}var i=0;return this._getMessages(this._getUserId(),!0).then(function(a){var b=this.messageArea.find(p.MESSAGES);return a.messages=a.messages.filter(function(a){var c=""+a.id+a.isread,d=b.find(p.MESSAGE+'[data-id="'+c+'"]');return!d.length}),i=a.messages.length,c.render("core_message/message_area_messages",a)}.bind(this)).then(function(d,e){i>0&&(d=a(d),d.find(p.BLOCKTIME).remove(),c.appendNodeContents(this.messageArea.find(p.MESSAGES),d,e),b&&this._scrollBottom(),this._numMessagesDisplayed+=i,this._timer.restart())}.bind(this)).always(function(){this._isLoadingMessages=!1}.bind(this)).fail(d.exception)},m.prototype._getMessages=function(a,c){var e={currentuserid:this.messageArea.getCurrentUserId(),otheruserid:a,limitfrom:this._numMessagesDisplayed,limitnum:this._numMessagesToRetrieve,newest:!0};c&&(e.createdfrom=this._earliestMessageTimestamp,e.limitfrom=0,e.limitnum=0);var f=b.call([{methodname:"core_message_data_for_messagearea_messages",args:e}]);return f[0].then(function(a){var b=a.messages;if(b&&b.length){var c=b[b.length-1];this._earliestMessageTimestamp?c.timecreated0?b.call(f)[f.length-1].then(function(){var b=null,c=this.messageArea.find(p.MESSAGE),d=c.last(),e=g[g.length-1];a.each(g,function(a,b){b.remove()}),d.data("id")===e.data("id")&&(b=this.messageArea.find(p.MESSAGE).last()),a.each(g,function(a,b){var c=b.data("blocktime");0===this.messageArea.find(p.MESSAGE+"[data-blocktime='"+c+"']").length&&this.messageArea.find(p.BLOCKTIME+"[data-blocktime='"+c+"']").remove()}.bind(this)),0===this.messageArea.find(p.MESSAGE).length&&this.messageArea.find(p.CONVERSATIONS+" "+p.CONTACT+"[data-userid='"+this._getUserId()+"']").remove(),this.messageArea.trigger(k.MESSAGESDELETED,[this._getUserId(),b])}.bind(this),d.exception):this.messageArea.trigger(k.MESSAGESDELETED,this._getUserId()),this._hideDeleteAction()},m.prototype._addScrollEventListener=function(a){this._scrollBottom(),this._numMessagesDisplayed=a,e.define(this.messageArea.find(p.MESSAGES),[e.events.scrollTop]),this.messageArea.onCustomEvent(e.events.scrollTop,this._loadMessages.bind(this))},m.prototype._deleteAllMessages=function(){this._confirmationModal?this._confirmationModal.show():j.get_strings([{key:"confirm"},{key:"deleteallconfirm",component:"message"}]).done(function(a){h.create({title:a[0],type:h.types.CONFIRM,body:a[1]},this.messageArea.find(p.DELETEALLMESSAGES)).done(function(a){this._confirmationModal=a,a.getRoot().on(i.yes,function(){var a=this._getUserId(),c={methodname:"core_message_delete_conversation",args:{userid:this.messageArea.getCurrentUserId(),otheruserid:a}};b.call([c])[0].then(function(){this.messageArea.find(p.MESSAGESAREA).empty(),this.messageArea.trigger(k.CONVERSATIONDELETED,a),this._hideDeleteAction()}.bind(this),d.exception)}.bind(this)),a.show()}.bind(this))}.bind(this))},m.prototype._hideDeleteAction=function(){this.messageArea.find(p.MESSAGE).removeAttr("role").removeAttr("aria-checked"),this.messageArea.find(p.MESSAGESAREA).removeClass("editing")},m.prototype._triggerCancelMessagesToDelete=function(){this.messageArea.trigger(k.CANCELDELETEMESSAGES)},m.prototype._addMessageToDom=function(){var a=b.call([{methodname:"core_message_data_for_messagearea_get_most_recent_message",args:{currentuserid:this.messageArea.getCurrentUserId(),otheruserid:this._getUserId()}}]);return a[0].then(function(a){return c.render("core_message/message_area_message",a)}).then(function(a,b){c.appendNodeContents(this.messageArea.find(p.MESSAGES),a,b),this.messageArea.find(p.SENDMESSAGETEXT).val("").trigger("input"),this._scrollBottom()}.bind(this)).fail(d.exception)},m.prototype._getUserId=function(){return this.messageArea.find(p.MESSAGES).data("userid")},m.prototype._scrollBottom=function(){var a=this.messageArea.find(p.MESSAGES);0!==a.length&&a.scrollTop(a[0].scrollHeight)},m.prototype._selectPreviousMessage=function(b,c){var d=a(b.target).closest(p.MESSAGE);do d=d.prev();while(d.length&&!d.is(p.MESSAGE));d.focus(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()},m.prototype._selectNextMessage=function(b,c){var d=a(b.target).closest(p.MESSAGE);do d=d.next();while(d.length&&!d.is(p.MESSAGE));d.focus(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()},m.prototype._setMessaging=function(b){a(b.target).closest(p.MESSAGERESPONSE).addClass("messaging")},m.prototype._clearMessaging=function(b){a(b.target).closest(p.MESSAGERESPONSE).removeClass("messaging")},m.prototype._startDeleting=function(a){var b=new g(this.messageArea);b.chooseMessagesToDelete(),a.preventDefault()},m.prototype._isEditing=function(){return this.messageArea.find(p.MESSAGESAREA).hasClass("editing")},m.prototype._toggleMessage=function(b){if(this._isEditing()){var c=a(b.target).closest(p.MESSAGE);"true"===c.attr("aria-checked")?c.attr("aria-checked","false"):c.attr("aria-checked","true")}},m.prototype._adjustMessagesAreaHeight=function(){var a=this.messageArea.find(p.MESSAGES),b=this.messageArea.find(p.MESSAGERESPONSE),c=b.outerHeight(),d=c-o,e=n-d;a.outerHeight(e)},m.prototype._sendMessageHandler=function(a,b){b.originalEvent.preventDefault(),this._sendMessage()},m.prototype._hideMessagingArea=function(){this.messageArea.find(p.MESSAGINGAREA).removeClass("show-messages").addClass("hide-messages")},m}); \ No newline at end of file diff --git a/message/amd/src/message_area_messages.js b/message/amd/src/message_area_messages.js index 51394caaf3186..a24ef0b0990e2 100644 --- a/message/amd/src/message_area_messages.js +++ b/message/amd/src/message_area_messages.js @@ -23,8 +23,9 @@ */ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/custom_interaction_events', 'core/auto_rows', 'core_message/message_area_actions', 'core/modal_factory', 'core/modal_events', - 'core/str', 'core_message/message_area_events'], - function($, Ajax, Templates, Notification, CustomEvents, AutoRows, Actions, ModalFactory, ModalEvents, Str, Events) { + 'core/str', 'core_message/message_area_events', 'core/backoff_timer'], + function($, Ajax, Templates, Notification, CustomEvents, AutoRows, Actions, ModalFactory, + ModalEvents, Str, Events, BackOffTimer) { /** @type {int} The message area default height. */ var MESSAGES_AREA_DEFAULT_HEIGHT = 500; @@ -77,6 +78,12 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust /** @type {Modal} the confirmation modal */ Messages.prototype._confirmationModal = null; + /** @type {int} the timestamp for the earliest visible message */ + Messages.prototype._earliestMessageTimestamp = 0; + + /** @type {BackOffTime} the backoff timer */ + Messages.prototype._timer = null; + /** @type {Messagearea} The messaging area object. */ Messages.prototype.messageArea = null; @@ -137,6 +144,14 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust if (messages.length) { this._addScrollEventListener(messages.find(SELECTORS.MESSAGE).length); } + + // Create a timer to poll the server for new messages. + this._timer = new BackOffTimer(function() { + this._loadNewMessages(); + }.bind(this)); + + // Start the timer. + this._timer.start(); }; /** @@ -150,6 +165,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust Messages.prototype._viewMessages = function(event, userid) { // We are viewing another user, or re-loading the panel, so set number of messages displayed to 0. this._numMessagesDisplayed = 0; + // Stop the existing timer so we can set up the new user's messages. + this._timer.stop(); + // Reset the earliest timestamp when we change the messages view. + this._earliestMessageTimestamp = 0; // Mark all the messages as read. var markMessagesAsRead = Ajax.call([{ @@ -183,6 +202,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust }).then(function(html, js) { Templates.replaceNodeContents(this.messageArea.find(SELECTORS.MESSAGESAREA), html, js); this._addScrollEventListener(numberreceived); + // Restart the poll timer. + this._timer.restart(); }.bind(this)).fail(Notification.exception); }; @@ -240,28 +261,130 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust }.bind(this)).fail(Notification.exception); }; + /** + * Loads and renders messages newer than the most recently seen messages. + * + * @return {Promise|boolean} The promise resolved when the messages have been loaded. + * @private + */ + Messages.prototype._loadNewMessages = function() { + if (this._isLoadingMessages) { + return false; + } + + // If we have no user id yet then bail early. + if (!this._getUserId()) { + return false; + } + + this._isLoadingMessages = true; + + // Only scroll the message window if the user hasn't scrolled up. + var shouldScrollBottom = false; + var messages = this.messageArea.find(SELECTORS.MESSAGES); + if (messages.length !== 0) { + var scrollTop = messages.scrollTop(); + var innerHeight = messages.innerHeight(); + var scrollHeight = messages[0].scrollHeight; + + if (scrollTop + innerHeight >= scrollHeight) { + shouldScrollBottom = true; + } + } + + // Keep track of the number of messages received. + var numberreceived = 0; + return this._getMessages(this._getUserId(), true).then(function(data) { + // Filter out any messages already rendered. + var messagesArea = this.messageArea.find(SELECTORS.MESSAGES); + data.messages = data.messages.filter(function(message) { + var id = "" + message.id + message.isread; + var result = messagesArea.find(SELECTORS.MESSAGE + '[data-id="' + id + '"]'); + return !result.length; + }); + + numberreceived = data.messages.length; + // We have the data - lets render the template with it. + return Templates.render('core_message/message_area_messages', data); + }.bind(this)).then(function(html, js) { + // Check if we got something to do. + if (numberreceived > 0) { + html = $(html); + // Remove the new block time as it's present above. + html.find(SELECTORS.BLOCKTIME).remove(); + // Show the new content. + Templates.appendNodeContents(this.messageArea.find(SELECTORS.MESSAGES), html, js); + // Scroll the new message into view. + if (shouldScrollBottom) { + this._scrollBottom(); + } + // Increment the number of messages displayed. + this._numMessagesDisplayed += numberreceived; + // Reset the poll timer because the user may be active. + this._timer.restart(); + } + }.bind(this)).always(function() { + // Mark that we are no longer busy loading data. + this._isLoadingMessages = false; + }.bind(this)).fail(Notification.exception); + }; + /** * Handles returning a list of messages to display. * * @param {int} userid + * @param {bool} fromTimestamp Load messages from the earliest known timestamp * @return {Promise} The promise resolved when the contact area has been rendered * @private */ - Messages.prototype._getMessages = function(userid) { + Messages.prototype._getMessages = function(userid, fromTimestamp) { + var args = { + currentuserid: this.messageArea.getCurrentUserId(), + otheruserid: userid, + limitfrom: this._numMessagesDisplayed, + limitnum: this._numMessagesToRetrieve, + newest: true + }; + + // If we're trying to load new messages since the message UI was + // rendered. Used for ajax polling while user is on the message UI. + if (fromTimestamp) { + args.createdfrom = this._earliestMessageTimestamp; + // Remove limit and offset. We want all new messages. + args.limitfrom = 0; + args.limitnum = 0; + } + // Call the web service to get our data. var promises = Ajax.call([{ methodname: 'core_message_data_for_messagearea_messages', - args: { - currentuserid: this.messageArea.getCurrentUserId(), - otheruserid: userid, - limitfrom: this._numMessagesDisplayed, - limitnum: this._numMessagesToRetrieve, - newest: true - } + args: args, }]); // Do stuff when we get data back. - return promises[0]; + return promises[0].then(function(data) { + var messages = data.messages; + + // Did we get any new messages? + if (messages && messages.length) { + var earliestMessage = messages[messages.length - 1]; + + // If we haven't set the timestamp yet then just use the earliest message. + if (!this._earliestMessageTimestamp) { + // Next request should be for the second after the most recent message we've seen. + this._earliestMessageTimestamp = earliestMessage.timecreated + 1; + // Update our record of the earliest known message for future requests. + } else if (earliestMessage.timecreated < this._earliestMessageTimestamp) { + // Next request should be for the second after the most recent message we've seen. + this._earliestMessageTimestamp = earliestMessage.timecreated + 1; + } + } + + return data; + }.bind(this)).fail(function() { + // Stop the timer if we received an error so that we don't keep spamming the server. + this._timer.stop(); + }.bind(this)).fail(Notification.exception); }; /** diff --git a/message/classes/api.php b/message/classes/api.php index eabe7d7379b0c..2d6150f03862f 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -291,11 +291,32 @@ public static function get_contacts($userid, $limitfrom = 0, $limitnum = 0) { * @param int $limitfrom * @param int $limitnum * @param string $sort + * @param int $createdfrom the timestamp from which the messages were created + * @param int $createdto the time up until which the message was created * @return array */ - public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0, $sort = 'timecreated ASC') { + public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0, + $sort = 'timecreated ASC', $createdfrom = 0, $createdto = 0) { + + if (!empty($createdfrom)) { + // Check the cache to see if we even need to do a DB query. + $cache = \cache::make('core', 'message_last_created'); + $ids = [$otheruserid, $userid]; + sort($ids); + $key = implode('_', $ids); + $lastcreated = $cache->get($key); + + // The last known message time is earlier than the one being requested so we can + // just return an empty result set rather than having to query the DB. + if ($lastcreated && $lastcreated < $createdfrom) { + return []; + } + } + $arrmessages = array(); - if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum, $sort)) { + if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum, + $sort, $createdfrom, $createdto)) { + $arrmessages = helper::create_messages($userid, $messages); } diff --git a/message/classes/helper.php b/message/classes/helper.php index 4fe71226aa9ab..3a4c70d730d10 100644 --- a/message/classes/helper.php +++ b/message/classes/helper.php @@ -43,10 +43,12 @@ class helper { * @param int $limitfrom * @param int $limitnum * @param string $sort + * @param int $createdfrom the time from which the message was created + * @param int $createdto the time up until which the message was created * @return array of messages */ public static function get_messages($userid, $otheruserid, $timedeleted = 0, $limitfrom = 0, $limitnum = 0, - $sort = 'timecreated ASC') { + $sort = 'timecreated ASC', $createdfrom = 0, $createdto = 0) { global $DB; $messageid = $DB->sql_concat("'message_'", 'id'); @@ -58,6 +60,7 @@ public static function get_messages($userid, $otheruserid, $timedeleted = 0, $li WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?) OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?)) AND notification = 0 + %where% UNION ALL SELECT {$messagereadid} AS fakeid, id, useridfrom, useridto, subject, fullmessage, fullmessagehtml, fullmessageformat, smallmessage, notification, timecreated, timeread @@ -65,11 +68,29 @@ public static function get_messages($userid, $otheruserid, $timedeleted = 0, $li WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?) OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?)) AND notification = 0 + %where% ORDER BY $sort"; - $params = array($userid, $otheruserid, $timedeleted, - $otheruserid, $userid, $timedeleted, - $userid, $otheruserid, $timedeleted, - $otheruserid, $userid, $timedeleted); + $params1 = array($userid, $otheruserid, $timedeleted, + $otheruserid, $userid, $timedeleted); + + $params2 = array($userid, $otheruserid, $timedeleted, + $otheruserid, $userid, $timedeleted); + $where = array(); + + if (!empty($createdfrom)) { + $where[] = 'AND timecreated >= ?'; + $params1[] = $createdfrom; + $params2[] = $createdfrom; + } + + if (!empty($createdto)) { + $where[] = 'AND timecreated <= ?'; + $params1[] = $createdto; + $params2[] = $createdto; + } + + $sql = str_replace('%where%', implode(' ', $where), $sql); + $params = array_merge($params1, $params2); return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); } diff --git a/message/classes/message_last_created_cache_source.php b/message/classes/message_last_created_cache_source.php new file mode 100644 index 0000000000000..ea0cd05df1b12 --- /dev/null +++ b/message/classes/message_last_created_cache_source.php @@ -0,0 +1,89 @@ +. + +/** + * Cache data source for the last created message between users. + * + * @package core_message + * @category cache + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + +defined('MOODLE_INTERNAL') || die(); + +/** + * Cache data source for the last created message between users. + * + * @package core_message + * @category cache + * @copyright 2016 Ryan Wyllie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class message_last_created_cache_source implements \cache_data_source { + + /** @var message_last_created_cache_source the singleton instance of this class. */ + protected static $instance = null; + + /** + * Returns an instance of the data source class that the cache can use for loading data using the other methods + * specified by the cache_data_source interface. + * + * @param cache_definition $definition + * @return object + */ + public static function get_instance_for_cache(cache_definition $definition) { + if (is_null(self::$instance)) { + self::$instance = new message_last_created_cache_source(); + } + return self::$instance; + } + + /** + * Loads the data for the key provided ready formatted for caching. + * + * @param string|int $key The key to load. + * @return mixed What ever data should be returned, or false if it can't be loaded. + */ + public function load_for_cache($key) { + list($userid1, $userid2) = explode('_', $key); + + $message = \core_message\api::get_most_recent_message($userid1, $userid2); + + if ($message) { + return $message->timecreated; + } else { + return null; + } + } + + /** + * Loads several keys for the cache. + * + * @param array $keys An array of keys each of which will be string|int. + * @return array An array of matching data items. + */ + public function load_many_for_cache(array $keys) { + $results = []; + + foreach ($keys as $key) { + $results[] = $this->load_for_cache($key); + } + + return $results; + } +} diff --git a/message/classes/output/messagearea/message.php b/message/classes/output/messagearea/message.php index 00813498ca449..5fc7e0579d915 100644 --- a/message/classes/output/messagearea/message.php +++ b/message/classes/output/messagearea/message.php @@ -107,6 +107,7 @@ public function export_for_template(\renderer_base $output) { $message->position = 'right'; } $message->timesent = userdate($this->timecreated, get_string('strftimetime')); + $message->timecreated = $this->timecreated; $message->isread = !empty($this->timeread) ? 1 : 0; return $message; diff --git a/message/externallib.php b/message/externallib.php index 2ecaf66b101b2..98b5634c9d6c7 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -515,6 +515,7 @@ private static function get_messagearea_message_structure() { 'blocktime' => new external_value(PARAM_NOTAGS, 'The time to display above the message'), 'position' => new external_value(PARAM_ALPHA, 'The position of the text'), 'timesent' => new external_value(PARAM_NOTAGS, 'The time the message was sent'), + 'timecreated' => new external_value(PARAM_INT, 'The timecreated timestamp for the message'), 'isread' => new external_value(PARAM_INT, 'Determines if the message was read or not'), ) ); @@ -900,6 +901,8 @@ public static function data_for_messagearea_messages_parameters() { 'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0), 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0), 'newest' => new external_value(PARAM_BOOL, 'Newest first?', VALUE_DEFAULT, false), + 'createdfrom' => new external_value(PARAM_INT, + 'The timestamp from which the messages were created', VALUE_DEFAULT, 0), ) ); } @@ -917,7 +920,7 @@ public static function data_for_messagearea_messages_parameters() { * @since 3.2 */ public static function data_for_messagearea_messages($currentuserid, $otheruserid, $limitfrom = 0, $limitnum = 0, - $newest = false) { + $newest = false, $createdfrom = 0) { global $CFG, $PAGE, $USER; // Check if messaging is enabled. @@ -932,7 +935,8 @@ public static function data_for_messagearea_messages($currentuserid, $otheruseri 'otheruserid' => $otheruserid, 'limitfrom' => $limitfrom, 'limitnum' => $limitnum, - 'newest' => $newest + 'newest' => $newest, + 'createdfrom' => $createdfrom, ); self::validate_parameters(self::data_for_messagearea_messages_parameters(), $params); self::validate_context($systemcontext); @@ -946,7 +950,29 @@ public static function data_for_messagearea_messages($currentuserid, $otheruseri } else { $sort = 'timecreated ASC'; } - $messages = \core_message\api::get_messages($currentuserid, $otheruserid, $limitfrom, $limitnum, $sort); + + // We need to enforce a one second delay on messages to avoid race conditions of current + // messages still being sent. + // + // There is a chance that we could request messages before the current time's + // second has elapsed and while other messages are being sent in that same second. In which + // case those messages will be lost. + // + // Instead we ignore the current time in the result set to ensure that second is allowed to finish. + if (!empty($createdfrom)) { + $createdto = time() - 1; + } else { + $createdto = 0; + } + + // No requesting messages from the current time, as stated above. + if ($createdfrom == time()) { + $mesages = []; + } else { + $messages = \core_message\api::get_messages($currentuserid, $otheruserid, $limitfrom, + $limitnum, $sort, $createdfrom, $createdto); + } + $messages = new \core_message\output\messagearea\messages($currentuserid, $otheruserid, $messages); $renderer = $PAGE->get_renderer('core_message'); diff --git a/message/tests/api_test.php b/message/tests/api_test.php index 67d898ed56900..15376e049370d 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -953,4 +953,129 @@ public function is_user_enabled() { $status = \core_message\api::is_processor_enabled($name); $this->assertEquals(1, $status); } + + /** + * Test retrieving messages by providing a minimum timecreated value. + */ + public function test_get_messages_created_from_only() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + $this->send_fake_message($user1, $user2, 'Message 1', 0, $time + 1); + $this->send_fake_message($user2, $user1, 'Message 2', 0, $time + 2); + $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3); + $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4); + + // Retrieve the messages. + $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time); + + // Confirm the message data is correct. + $this->assertEquals(4, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + $message4 = $messages[3]; + + $this->assertContains('Message 1', $message1->text); + $this->assertContains('Message 2', $message2->text); + $this->assertContains('Message 3', $message3->text); + $this->assertContains('Message 4', $message4->text); + + // Retrieve the messages. + $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 3); + + // Confirm the message data is correct. + $this->assertEquals(2, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + + $this->assertContains('Message 3', $message1->text); + $this->assertContains('Message 4', $message2->text); + } + + /** + * Test retrieving messages by providing a maximum timecreated value. + */ + public function test_get_messages_created_to_only() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + $this->send_fake_message($user1, $user2, 'Message 1', 0, $time + 1); + $this->send_fake_message($user2, $user1, 'Message 2', 0, $time + 2); + $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3); + $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4); + + // Retrieve the messages. + $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 4); + + // Confirm the message data is correct. + $this->assertEquals(4, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + $message4 = $messages[3]; + + $this->assertContains('Message 1', $message1->text); + $this->assertContains('Message 2', $message2->text); + $this->assertContains('Message 3', $message3->text); + $this->assertContains('Message 4', $message4->text); + + // Retrieve the messages. + $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 2); + + // Confirm the message data is correct. + $this->assertEquals(2, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + + $this->assertContains('Message 1', $message1->text); + $this->assertContains('Message 2', $message2->text); + } + + /** + * Test retrieving messages by providing a minimum and maximum timecreated value. + */ + public function test_get_messages_created_from_and_to() { + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + // The person doing the search. + $this->setUser($user1); + + // Send some messages back and forth. + $time = 1; + $this->send_fake_message($user1, $user2, 'Message 1', 0, $time + 1); + $this->send_fake_message($user2, $user1, 'Message 2', 0, $time + 2); + $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3); + $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4); + + // Retrieve the messages. + $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 2, $time + 3); + + // Confirm the message data is correct. + $this->assertEquals(2, count($messages)); + + $message1 = $messages[0]; + $message2 = $messages[1]; + + $this->assertContains('Message 2', $message1->text); + $this->assertContains('Message 3', $message2->text); + } } diff --git a/message/tests/externallib_test.php b/message/tests/externallib_test.php index f28c50e7d036d..c739b1a4e3664 100644 --- a/message/tests/externallib_test.php +++ b/message/tests/externallib_test.php @@ -1979,6 +1979,46 @@ public function test_messagearea_messages() { $this->assertContains('Word.', $message4['text']); } + /** + * Tests retrieving messages. + */ + public function test_messagearea_messages_createfrom() { + $this->resetAfterTest(true); + + // Create some users. + $user1 = self::getDataGenerator()->create_user(); + $user2 = self::getDataGenerator()->create_user(); + + // The person asking for the messages. + $this->setUser($user1); + + // Send some messages back and forth. + $time = time(); + $this->send_message($user1, $user2, 'Message 1', 0, $time - 4); + $this->send_message($user2, $user1, 'Message 2', 0, $time - 3); + $this->send_message($user1, $user2, 'Message 3', 0, $time - 2); + $this->send_message($user2, $user1, 'Message 4', 0, $time - 1); + + // Retrieve the messages. + $result = core_message_external::data_for_messagearea_messages($user1->id, $user2->id, 0, 0, false, $time - 3); + + // We need to execute the return values cleaning process to simulate the web service server. + $result = external_api::clean_returnvalue(core_message_external::data_for_messagearea_messages_returns(), + $result); + + // Confirm the message data is correct. We shouldn't get 'Message 1' back. + $messages = $result['messages']; + $this->assertCount(3, $messages); + + $message1 = $messages[0]; + $message2 = $messages[1]; + $message3 = $messages[2]; + + $this->assertContains('Message 2', $message1['text']); + $this->assertContains('Message 3', $message2['text']); + $this->assertContains('Message 4', $message3['text']); + } + /** * Tests retrieving messages as another user. */ diff --git a/version.php b/version.php index e49f805713dd1..e23de8f66c731 100644 --- a/version.php +++ b/version.php @@ -29,11 +29,11 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2016111500.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2016111600.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. -$release = '3.2beta+ (Build: 20161115)'; // Human-friendly version name +$release = '3.2beta+ (Build: 20161116)'; // Human-friendly version name $branch = '32'; // This version's branch. $maturity = MATURITY_BETA; // This version's maturity level. From ffd7798c960bc4f26d51b806f95a4877844ddd74 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Thu, 10 Nov 2016 12:47:20 +0800 Subject: [PATCH 2/3] MDL-56139 core: changes after peer review - No longer use the Fibonacci sequence for delaying the timeout. It is too aggressive. - The backoff_timer AMD module now expects the callback AND the backoff function to be passed to the constructor. - Added ability to specify polling frequency in config.php. - Added helper function to return the cache key. - Reworded the parameters for clarity. --- lang/en/cache.php | 2 +- lib/amd/build/backoff_timer.min.js | 2 +- lib/amd/src/backoff_timer.js | 133 +++++++----------- lib/db/caches.php | 7 +- lib/messagelib.php | 7 +- message/amd/build/message_area.min.js | 2 +- .../amd/build/message_area_messages.min.js | 2 +- message/amd/src/message_area.js | 17 ++- message/amd/src/message_area_messages.js | 30 ++-- message/classes/api.php | 18 ++- message/classes/helper.php | 31 ++-- .../output/messagearea/message_area.php | 27 +++- ...hp => time_last_message_between_users.php} | 17 +-- message/externallib.php | 18 +-- message/index.php | 5 +- message/lib.php | 7 + message/templates/message_area.mustache | 2 +- message/tests/api_test.php | 16 +-- message/tests/externallib_test.php | 4 +- 19 files changed, 190 insertions(+), 157 deletions(-) rename message/classes/{message_last_created_cache_source.php => time_last_message_between_users.php} (79%) diff --git a/lang/en/cache.php b/lang/en/cache.php index ae490cbaadb1e..b97ae2b189e70 100644 --- a/lang/en/cache.php +++ b/lang/en/cache.php @@ -52,7 +52,7 @@ $string['cachedef_groupdata'] = 'Course group information'; $string['cachedef_htmlpurifier'] = 'HTML Purifier - cleaned content'; $string['cachedef_langmenu'] = 'List of available languages'; -$string['cachedef_message_last_created'] = 'Time created for most recent message between users'; +$string['cachedef_message_time_last_message_between_users'] = 'Time created for most recent message between users'; $string['cachedef_locking'] = 'Locking'; $string['cachedef_message_processors_enabled'] = "Message processors enabled status"; $string['cachedef_navigation_expandcourse'] = 'Navigation expandable courses'; diff --git a/lib/amd/build/backoff_timer.min.js b/lib/amd/build/backoff_timer.min.js index b1d81162ea102..bb80b45d1235f 100644 --- a/lib/amd/build/backoff_timer.min.js +++ b/lib/amd/build/backoff_timer.min.js @@ -1 +1 @@ -define(function(){var a=1e3,b=function(b,c){if(!b)return a;if(c.length){var d=c[c.length-1];return b+d}return a},c=function(a){this.reset(),this.setCallback(a),this.setBackOffFunction(b)};return c.prototype.setCallback=function(a){return this.callback=a,this},c.prototype.getCallback=function(){return this.callback},c.prototype.setBackOffFunction=function(a){return this.backOffFunction=a,this},c.prototype.getBackOffFunction=function(){return this.backOffFunction},c.prototype.generateNextTime=function(){var a=this.getBackOffFunction().call(this.getBackOffFunction(),this.time,this.previousTimes);return this.previousTimes.push(this.time),this.time=a,a},c.prototype.reset=function(){return this.time=null,this.previousTimes=[],this.stop(),this},c.prototype.stop=function(){return this.timeout&&(window.clearTimeout(this.timeout),this.timeout=null),this},c.prototype.start=function(){if(!this.timeout){var a=this.generateNextTime();this.timeout=window.setTimeout(function(){this.getCallback().call(),this.stop(),this.start()}.bind(this),a)}return this},c.prototype.restart=function(){return this.reset().start()},c}); \ No newline at end of file +define(function(){var a=function(a,b){this.callback=a,this.backOffFunction=b};return a.prototype.callback=null,a.prototype.backOffFunction=null,a.prototype.time=null,a.prototype.timeout=null,a.prototype.generateNextTime=function(){var a=this.backOffFunction(this.time);return this.time=a,a},a.prototype.reset=function(){return this.time=null,this.stop(),this},a.prototype.stop=function(){return this.timeout&&(window.clearTimeout(this.timeout),this.timeout=null),this},a.prototype.start=function(){if(!this.timeout){var a=this.generateNextTime();this.timeout=window.setTimeout(function(){this.callback(),this.stop(),this.start()}.bind(this),a)}return this},a.prototype.restart=function(){return this.reset().start()},a.getIncrementalCallback=function(a,b,c,d){return function(e){return e?e+b>c?d:e+b:a}},a}); \ No newline at end of file diff --git a/lib/amd/src/backoff_timer.js b/lib/amd/src/backoff_timer.js index 992a322d70660..da0fabdb0861d 100644 --- a/lib/amd/src/backoff_timer.js +++ b/lib/amd/src/backoff_timer.js @@ -25,92 +25,36 @@ */ define(function() { - // Default to one second. - var DEFAULT_TIME = 1000; - - /** - * The default back off function for the timer. It uses the Fibonacci - * sequence to determine what the next timeout value should be. - * - * @param {(int|null)} time The current timeout value or null if none set - * @param {array} previousTimes An array containing all previous timeout values - * @return {int} The new timeout value - */ - var fibonacciBackOff = function(time, previousTimes) { - if (!time) { - return DEFAULT_TIME; - } - - if (previousTimes.length) { - var lastTime = previousTimes[previousTimes.length - 1]; - return time + lastTime; - } else { - return DEFAULT_TIME; - } - }; - /** * Constructor for the back off timer. * * @param {function} callback The function to execute after each tick + * @param {function} backoffFunction The function to determine what the next timeout value should be */ - var Timer = function(callback) { - this.reset(); - this.setCallback(callback); - // Set the default backoff function to be the Fibonacci sequence. - this.setBackOffFunction(fibonacciBackOff); + var BackoffTimer = function(callback, backoffFunction) { + this.callback = callback; + this.backOffFunction = backoffFunction; }; /** - * Set the callback function to be executed after each tick of the - * timer. - * - * @method setCallback - * @param {function} callback The callback function - * @return {object} this + * @type {function} callback The function to execute after each tick */ - Timer.prototype.setCallback = function(callback) { - this.callback = callback; - - return this; - }; + BackoffTimer.prototype.callback = null; /** - * Get the callback function for this timer. - * - * @method getCallback - * @return {function} + * @type {function} backoffFunction The function to determine what the next timeout value should be */ - Timer.prototype.getCallback = function() { - return this.callback; - }; + BackoffTimer.prototype.backOffFunction = null; /** - * Set the function to be used when calculating the back off time - * for each tick of the timer. - * - * The back off function will be given two parameters: the current - * time and an array containing all previous times. - * - * @method setBackOffFunction - * @param {function} backOffFunction The function to calculate back off times - * @return {object} this + * @type {int} time The timeout value to use */ - Timer.prototype.setBackOffFunction = function(backOffFunction) { - this.backOffFunction = backOffFunction; - - return this; - }; + BackoffTimer.prototype.time = null; /** - * Get the current back off function. - * - * @method getBackOffFunction - * @return {function} + * @type {numeric} timeout The timeout identifier */ - Timer.prototype.getBackOffFunction = function() { - return this.backOffFunction; - }; + BackoffTimer.prototype.timeout = null; /** * Generate the next timeout in the back off time sequence @@ -122,13 +66,8 @@ define(function() { * @method generateNextTime * @return {int} The new timeout value (in milliseconds) */ - Timer.prototype.generateNextTime = function() { - var newTime = this.getBackOffFunction().call( - this.getBackOffFunction(), - this.time, - this.previousTimes - ); - this.previousTimes.push(this.time); + BackoffTimer.prototype.generateNextTime = function() { + var newTime = this.backOffFunction(this.time); this.time = newTime; return newTime; @@ -140,9 +79,8 @@ define(function() { * @method reset * @return {object} this */ - Timer.prototype.reset = function() { + BackoffTimer.prototype.reset = function() { this.time = null; - this.previousTimes = []; this.stop(); return this; @@ -154,7 +92,7 @@ define(function() { * @method stop * @return {object} this */ - Timer.prototype.stop = function() { + BackoffTimer.prototype.stop = function() { if (this.timeout) { window.clearTimeout(this.timeout); this.timeout = null; @@ -175,12 +113,12 @@ define(function() { * @method start * @return {object} this */ - Timer.prototype.start = function() { + BackoffTimer.prototype.start = function() { // If we haven't already started. if (!this.timeout) { var time = this.generateNextTime(); this.timeout = window.setTimeout(function() { - this.getCallback().call(); + this.callback(); // Clear the existing timer. this.stop(); // Start the next timer. @@ -198,9 +136,40 @@ define(function() { * @method restart * @return {object} this */ - Timer.prototype.restart = function() { + BackoffTimer.prototype.restart = function() { return this.reset().start(); }; - return Timer; + /** + * Returns an incremental function for the timer. + * + * @param {int} minamount The minimum amount of time we wait before checking + * @param {int} incrementamount The amount to increment the timer by + * @param {int} maxamount The max amount to ever increment to + * @param {int} timeoutamount The timeout to use once we reach the max amount + * @return {function} + */ + BackoffTimer.getIncrementalCallback = function(minamount, incrementamount, maxamount, timeoutamount) { + + /** + * An incremental function for the timer. + * + * @param {(int|null)} time The current timeout value or null if none set + * @return {int} The new timeout value + */ + return function(time) { + if (!time) { + return minamount; + } + + // Don't go over the max amount. + if (time + incrementamount > maxamount) { + return timeoutamount; + } + + return time + incrementamount; + }; + }; + + return BackoffTimer; }); diff --git a/lib/db/caches.php b/lib/db/caches.php index 49e509a818654..9a8e9f24e5d31 100644 --- a/lib/db/caches.php +++ b/lib/db/caches.php @@ -302,12 +302,11 @@ 'staticaccelerationsize' => 3 ), - // Cache for storing the user's last received message time. - 'message_last_created' => array( + // Caches the time of the last message between two users. + 'message_time_last_message_between_users' => array( 'mode' => cache_store::MODE_APPLICATION, 'simplekeys' => true, // The id of the sender and recipient is used. 'simplevalues' => true, - 'datasource' => 'message_last_created_cache_source', - 'datasourcefile' => 'message/classes/message_last_created_cache_source.php' + 'datasource' => '\core_message\time_last_message_between_users', ), ); diff --git a/lib/messagelib.php b/lib/messagelib.php index 9bfad534db6f4..5347f173b6116 100644 --- a/lib/messagelib.php +++ b/lib/messagelib.php @@ -237,10 +237,9 @@ function message_send($eventdata) { // Only cache messages, not notifications. if (empty($savemessage->notification)) { // Cache the timecreated value of the last message between these two users. - $cache = cache::make('core', 'message_last_created'); - $ids = [$savemessage->useridfrom, $savemessage->useridto]; - sort($ids); - $key = implode('_', $ids); + $cache = cache::make('core', 'message_time_last_message_between_users'); + $key = \core_message\helper::get_last_message_time_created_cache_key($savemessage->useridfrom, + $savemessage->useridto); $cache->set($key, $savemessage->timecreated); } diff --git a/message/amd/build/message_area.min.js b/message/amd/build/message_area.min.js index 942dc1cb22d1b..b117ee3e361f1 100644 --- a/message/amd/build/message_area.min.js +++ b/message/amd/build/message_area.min.js @@ -1 +1 @@ -define(["jquery","core_message/message_area_contacts","core_message/message_area_messages","core_message/message_area_profile","core_message/message_area_tabs","core_message/message_area_search"],function(a,b,c,d,e,f){function g(b){this.node=a(b),this._init()}return g.prototype.node=null,g.prototype._init=function(){new b(this),new c(this),new d(this),new e(this),new f(this)},g.prototype.onDelegateEvent=function(a,b,c){this.node.on(a,b,c)},g.prototype.onCustomEvent=function(a,b){this.node.on(a,b)},g.prototype.trigger=function(a,b){"undefined"==typeof b&&(b=""),this.node.trigger(a,b)},g.prototype.find=function(a){return this.node.find(a)},g.prototype.getCurrentUserId=function(){return this.node.data("userid")},g}); \ No newline at end of file +define(["jquery","core_message/message_area_contacts","core_message/message_area_messages","core_message/message_area_profile","core_message/message_area_tabs","core_message/message_area_search"],function(a,b,c,d,e,f){function g(b,c,d,e){this.node=a(b),this.pollmin=c,this.pollmax=d,this.polltimeout=e,this._init()}return g.prototype.node=null,g.prototype.pollmin=null,g.prototype.pollmax=null,g.prototype.polltimeout=null,g.prototype._init=function(){new b(this),new c(this),new d(this),new e(this),new f(this)},g.prototype.onDelegateEvent=function(a,b,c){this.node.on(a,b,c)},g.prototype.onCustomEvent=function(a,b){this.node.on(a,b)},g.prototype.trigger=function(a,b){"undefined"==typeof b&&(b=""),this.node.trigger(a,b)},g.prototype.find=function(a){return this.node.find(a)},g.prototype.getCurrentUserId=function(){return this.node.data("userid")},g}); \ No newline at end of file diff --git a/message/amd/build/message_area_messages.min.js b/message/amd/build/message_area_messages.min.js index 4ba61b0677a40..09a54373a3f2e 100644 --- a/message/amd/build/message_area_messages.min.js +++ b/message/amd/build/message_area_messages.min.js @@ -1 +1 @@ -define(["jquery","core/ajax","core/templates","core/notification","core/custom_interaction_events","core/auto_rows","core_message/message_area_actions","core/modal_factory","core/modal_events","core/str","core_message/message_area_events","core/backoff_timer"],function(a,b,c,d,e,f,g,h,i,j,k,l){function m(a){this.messageArea=a,this._init()}var n=500,o=50,p={BLOCKTIME:"[data-region='blocktime']",CANCELDELETEMESSAGES:"[data-action='cancel-delete-messages']",CONTACT:"[data-region='contact']",CONVERSATIONS:"[data-region='contacts'][data-region-content='conversations']",DELETEALLMESSAGES:"[data-action='delete-all-messages']",DELETEMESSAGES:"[data-action='delete-messages']",LOADINGICON:".loading-icon",MESSAGE:"[data-region='message']",MESSAGERESPONSE:"[data-region='response']",MESSAGES:"[data-region='messages']",MESSAGESAREA:"[data-region='messages-area']",MESSAGINGAREA:"[data-region='messaging-area']",SENDMESSAGE:"[data-action='send-message']",SENDMESSAGETEXT:"[data-region='send-message-txt']",SHOWCONTACTS:"[data-action='show-contacts']",STARTDELETEMESSAGES:"[data-action='start-delete-messages']"};return m.prototype._isSendingMessage=!1,m.prototype._isLoadingMessages=!1,m.prototype._numMessagesDisplayed=0,m.prototype._numMessagesToRetrieve=20,m.prototype._confirmationModal=null,m.prototype._earliestMessageTimestamp=0,m.prototype._timer=null,m.prototype.messageArea=null,m.prototype._init=function(){e.define(this.messageArea.node,[e.events.activate,e.events.up,e.events.down,e.events.enter]),a(window).height()<=670&&(n=400),f.init(this.messageArea.node),this.messageArea.onCustomEvent(k.CONVERSATIONSELECTED,this._viewMessages.bind(this)),this.messageArea.onCustomEvent(k.SENDMESSAGE,this._viewMessages.bind(this)),this.messageArea.onCustomEvent(k.CHOOSEMESSAGESTODELETE,this._chooseMessagesToDelete.bind(this)),this.messageArea.onCustomEvent(k.CANCELDELETEMESSAGES,this._hideDeleteAction.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.SENDMESSAGE,this._sendMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.STARTDELETEMESSAGES,this._startDeleting.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.DELETEMESSAGES,this._deleteMessages.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.DELETEALLMESSAGES,this._deleteAllMessages.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.CANCELDELETEMESSAGES,this._triggerCancelMessagesToDelete.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.MESSAGE,this._toggleMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.SHOWCONTACTS,this._hideMessagingArea.bind(this)),this.messageArea.onDelegateEvent(e.events.up,p.MESSAGE,this._selectPreviousMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.down,p.MESSAGE,this._selectNextMessage.bind(this)),this.messageArea.onDelegateEvent("focus",p.SENDMESSAGETEXT,this._setMessaging.bind(this)),this.messageArea.onDelegateEvent("blur",p.SENDMESSAGETEXT,this._clearMessaging.bind(this)),this.messageArea.onDelegateEvent(e.events.enter,p.SENDMESSAGETEXT,this._sendMessageHandler.bind(this)),a(document).on(f.events.ROW_CHANGE,this._adjustMessagesAreaHeight.bind(this));var b=this.messageArea.find(p.MESSAGES);b.length&&this._addScrollEventListener(b.find(p.MESSAGE).length),this._timer=new l(function(){this._loadNewMessages()}.bind(this)),this._timer.start()},m.prototype._viewMessages=function(e,f){this._numMessagesDisplayed=0,this._timer.stop(),this._earliestMessageTimestamp=0;var g=b.call([{methodname:"core_message_mark_all_messages_as_read",args:{useridto:this.messageArea.getCurrentUserId(),useridfrom:f}}]),h=0;return c.render("core/loading",{}).then(function(a,b){return c.replaceNodeContents(this.messageArea.find(p.MESSAGESAREA),a,b),g[0]}.bind(this)).then(function(){var b=this.messageArea.find(p.CONVERSATIONS+" "+p.CONTACT+"[data-userid='"+f+"']");return b.hasClass("unread")&&(b.removeClass("unread"),a(document).trigger("messagearea:conversationselected",f)),this._getMessages(f)}.bind(this)).then(function(a){return h=a.messages.length,c.render("core_message/message_area_messages_area",a)}).then(function(a,b){c.replaceNodeContents(this.messageArea.find(p.MESSAGESAREA),a,b),this._addScrollEventListener(h),this._timer.restart()}.bind(this)).fail(d.exception)},m.prototype._loadMessages=function(){if(this._isLoadingMessages)return!1;this._isLoadingMessages=!0;var b=0;return c.render("core/loading",{}).then(function(a,b){return c.prependNodeContents(this.messageArea.find(p.MESSAGES),"
"+a+"
",b),this._getMessages(this._getUserId())}.bind(this)).then(function(a){return b=a.messages.length,c.render("core_message/message_area_messages",a)}).then(function(d,e){if(this.messageArea.find(p.MESSAGES+" "+p.LOADINGICON).remove(),b>0){var f=this.messageArea.node.find(p.BLOCKTIME+":first"),g=a(d).find(p.BLOCKTIME+":first").addBack();f.html()==g.html()&&f.remove();var h=this.messageArea.find(p.MESSAGES)[0].scrollHeight;c.prependNodeContents(this.messageArea.find(p.MESSAGES),d,e);var i=this.messageArea.find(p.MESSAGES)[0].scrollHeight;this.messageArea.find(p.MESSAGES).scrollTop(i-h),this._numMessagesDisplayed+=b}this._isLoadingMessages=!1}.bind(this)).fail(d.exception)},m.prototype._loadNewMessages=function(){if(this._isLoadingMessages)return!1;if(!this._getUserId())return!1;this._isLoadingMessages=!0;var b=!1,e=this.messageArea.find(p.MESSAGES);if(0!==e.length){var f=e.scrollTop(),g=e.innerHeight(),h=e[0].scrollHeight;f+g>=h&&(b=!0)}var i=0;return this._getMessages(this._getUserId(),!0).then(function(a){var b=this.messageArea.find(p.MESSAGES);return a.messages=a.messages.filter(function(a){var c=""+a.id+a.isread,d=b.find(p.MESSAGE+'[data-id="'+c+'"]');return!d.length}),i=a.messages.length,c.render("core_message/message_area_messages",a)}.bind(this)).then(function(d,e){i>0&&(d=a(d),d.find(p.BLOCKTIME).remove(),c.appendNodeContents(this.messageArea.find(p.MESSAGES),d,e),b&&this._scrollBottom(),this._numMessagesDisplayed+=i,this._timer.restart())}.bind(this)).always(function(){this._isLoadingMessages=!1}.bind(this)).fail(d.exception)},m.prototype._getMessages=function(a,c){var e={currentuserid:this.messageArea.getCurrentUserId(),otheruserid:a,limitfrom:this._numMessagesDisplayed,limitnum:this._numMessagesToRetrieve,newest:!0};c&&(e.createdfrom=this._earliestMessageTimestamp,e.limitfrom=0,e.limitnum=0);var f=b.call([{methodname:"core_message_data_for_messagearea_messages",args:e}]);return f[0].then(function(a){var b=a.messages;if(b&&b.length){var c=b[b.length-1];this._earliestMessageTimestamp?c.timecreated0?b.call(f)[f.length-1].then(function(){var b=null,c=this.messageArea.find(p.MESSAGE),d=c.last(),e=g[g.length-1];a.each(g,function(a,b){b.remove()}),d.data("id")===e.data("id")&&(b=this.messageArea.find(p.MESSAGE).last()),a.each(g,function(a,b){var c=b.data("blocktime");0===this.messageArea.find(p.MESSAGE+"[data-blocktime='"+c+"']").length&&this.messageArea.find(p.BLOCKTIME+"[data-blocktime='"+c+"']").remove()}.bind(this)),0===this.messageArea.find(p.MESSAGE).length&&this.messageArea.find(p.CONVERSATIONS+" "+p.CONTACT+"[data-userid='"+this._getUserId()+"']").remove(),this.messageArea.trigger(k.MESSAGESDELETED,[this._getUserId(),b])}.bind(this),d.exception):this.messageArea.trigger(k.MESSAGESDELETED,this._getUserId()),this._hideDeleteAction()},m.prototype._addScrollEventListener=function(a){this._scrollBottom(),this._numMessagesDisplayed=a,e.define(this.messageArea.find(p.MESSAGES),[e.events.scrollTop]),this.messageArea.onCustomEvent(e.events.scrollTop,this._loadMessages.bind(this))},m.prototype._deleteAllMessages=function(){this._confirmationModal?this._confirmationModal.show():j.get_strings([{key:"confirm"},{key:"deleteallconfirm",component:"message"}]).done(function(a){h.create({title:a[0],type:h.types.CONFIRM,body:a[1]},this.messageArea.find(p.DELETEALLMESSAGES)).done(function(a){this._confirmationModal=a,a.getRoot().on(i.yes,function(){var a=this._getUserId(),c={methodname:"core_message_delete_conversation",args:{userid:this.messageArea.getCurrentUserId(),otheruserid:a}};b.call([c])[0].then(function(){this.messageArea.find(p.MESSAGESAREA).empty(),this.messageArea.trigger(k.CONVERSATIONDELETED,a),this._hideDeleteAction()}.bind(this),d.exception)}.bind(this)),a.show()}.bind(this))}.bind(this))},m.prototype._hideDeleteAction=function(){this.messageArea.find(p.MESSAGE).removeAttr("role").removeAttr("aria-checked"),this.messageArea.find(p.MESSAGESAREA).removeClass("editing")},m.prototype._triggerCancelMessagesToDelete=function(){this.messageArea.trigger(k.CANCELDELETEMESSAGES)},m.prototype._addMessageToDom=function(){var a=b.call([{methodname:"core_message_data_for_messagearea_get_most_recent_message",args:{currentuserid:this.messageArea.getCurrentUserId(),otheruserid:this._getUserId()}}]);return a[0].then(function(a){return c.render("core_message/message_area_message",a)}).then(function(a,b){c.appendNodeContents(this.messageArea.find(p.MESSAGES),a,b),this.messageArea.find(p.SENDMESSAGETEXT).val("").trigger("input"),this._scrollBottom()}.bind(this)).fail(d.exception)},m.prototype._getUserId=function(){return this.messageArea.find(p.MESSAGES).data("userid")},m.prototype._scrollBottom=function(){var a=this.messageArea.find(p.MESSAGES);0!==a.length&&a.scrollTop(a[0].scrollHeight)},m.prototype._selectPreviousMessage=function(b,c){var d=a(b.target).closest(p.MESSAGE);do d=d.prev();while(d.length&&!d.is(p.MESSAGE));d.focus(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()},m.prototype._selectNextMessage=function(b,c){var d=a(b.target).closest(p.MESSAGE);do d=d.next();while(d.length&&!d.is(p.MESSAGE));d.focus(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()},m.prototype._setMessaging=function(b){a(b.target).closest(p.MESSAGERESPONSE).addClass("messaging")},m.prototype._clearMessaging=function(b){a(b.target).closest(p.MESSAGERESPONSE).removeClass("messaging")},m.prototype._startDeleting=function(a){var b=new g(this.messageArea);b.chooseMessagesToDelete(),a.preventDefault()},m.prototype._isEditing=function(){return this.messageArea.find(p.MESSAGESAREA).hasClass("editing")},m.prototype._toggleMessage=function(b){if(this._isEditing()){var c=a(b.target).closest(p.MESSAGE);"true"===c.attr("aria-checked")?c.attr("aria-checked","false"):c.attr("aria-checked","true")}},m.prototype._adjustMessagesAreaHeight=function(){var a=this.messageArea.find(p.MESSAGES),b=this.messageArea.find(p.MESSAGERESPONSE),c=b.outerHeight(),d=c-o,e=n-d;a.outerHeight(e)},m.prototype._sendMessageHandler=function(a,b){b.originalEvent.preventDefault(),this._sendMessage()},m.prototype._hideMessagingArea=function(){this.messageArea.find(p.MESSAGINGAREA).removeClass("show-messages").addClass("hide-messages")},m}); \ No newline at end of file +define(["jquery","core/ajax","core/templates","core/notification","core/custom_interaction_events","core/auto_rows","core_message/message_area_actions","core/modal_factory","core/modal_events","core/str","core_message/message_area_events","core/backoff_timer"],function(a,b,c,d,e,f,g,h,i,j,k,l){function m(a){this.messageArea=a,this._init()}var n=500,o=50,p={BLOCKTIME:"[data-region='blocktime']",CANCELDELETEMESSAGES:"[data-action='cancel-delete-messages']",CONTACT:"[data-region='contact']",CONVERSATIONS:"[data-region='contacts'][data-region-content='conversations']",DELETEALLMESSAGES:"[data-action='delete-all-messages']",DELETEMESSAGES:"[data-action='delete-messages']",LOADINGICON:".loading-icon",MESSAGE:"[data-region='message']",MESSAGERESPONSE:"[data-region='response']",MESSAGES:"[data-region='messages']",MESSAGESAREA:"[data-region='messages-area']",MESSAGINGAREA:"[data-region='messaging-area']",SENDMESSAGE:"[data-action='send-message']",SENDMESSAGETEXT:"[data-region='send-message-txt']",SHOWCONTACTS:"[data-action='show-contacts']",STARTDELETEMESSAGES:"[data-action='start-delete-messages']"},q=1e3;return m.prototype._isSendingMessage=!1,m.prototype._isLoadingMessages=!1,m.prototype._numMessagesDisplayed=0,m.prototype._numMessagesToRetrieve=20,m.prototype._confirmationModal=null,m.prototype._earliestMessageTimestamp=0,m.prototype._backoffTimer=null,m.prototype.messageArea=null,m.prototype._init=function(){e.define(this.messageArea.node,[e.events.activate,e.events.up,e.events.down,e.events.enter]),a(window).height()<=670&&(n=400),f.init(this.messageArea.node),this.messageArea.onCustomEvent(k.CONVERSATIONSELECTED,this._viewMessages.bind(this)),this.messageArea.onCustomEvent(k.SENDMESSAGE,this._viewMessages.bind(this)),this.messageArea.onCustomEvent(k.CHOOSEMESSAGESTODELETE,this._chooseMessagesToDelete.bind(this)),this.messageArea.onCustomEvent(k.CANCELDELETEMESSAGES,this._hideDeleteAction.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.SENDMESSAGE,this._sendMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.STARTDELETEMESSAGES,this._startDeleting.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.DELETEMESSAGES,this._deleteMessages.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.DELETEALLMESSAGES,this._deleteAllMessages.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.CANCELDELETEMESSAGES,this._triggerCancelMessagesToDelete.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.MESSAGE,this._toggleMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.activate,p.SHOWCONTACTS,this._hideMessagingArea.bind(this)),this.messageArea.onDelegateEvent(e.events.up,p.MESSAGE,this._selectPreviousMessage.bind(this)),this.messageArea.onDelegateEvent(e.events.down,p.MESSAGE,this._selectNextMessage.bind(this)),this.messageArea.onDelegateEvent("focus",p.SENDMESSAGETEXT,this._setMessaging.bind(this)),this.messageArea.onDelegateEvent("blur",p.SENDMESSAGETEXT,this._clearMessaging.bind(this)),this.messageArea.onDelegateEvent(e.events.enter,p.SENDMESSAGETEXT,this._sendMessageHandler.bind(this)),a(document).on(f.events.ROW_CHANGE,this._adjustMessagesAreaHeight.bind(this));var b=this.messageArea.find(p.MESSAGES);b.length&&this._addScrollEventListener(b.find(p.MESSAGE).length),this._backoffTimer=new l(this._loadNewMessages.bind(this),l.getIncrementalCallback(this.messageArea.pollmin*q,q,this.messageArea.pollmax*q,this.messageArea.polltimeout*q)),this._backoffTimer.start()},m.prototype._viewMessages=function(e,f){this._numMessagesDisplayed=0,this._backoffTimer.stop(),this._earliestMessageTimestamp=0;var g=b.call([{methodname:"core_message_mark_all_messages_as_read",args:{useridto:this.messageArea.getCurrentUserId(),useridfrom:f}}]),h=0;return c.render("core/loading",{}).then(function(a,b){return c.replaceNodeContents(this.messageArea.find(p.MESSAGESAREA),a,b),g[0]}.bind(this)).then(function(){var b=this.messageArea.find(p.CONVERSATIONS+" "+p.CONTACT+"[data-userid='"+f+"']");return b.hasClass("unread")&&(b.removeClass("unread"),a(document).trigger("messagearea:conversationselected",f)),this._getMessages(f)}.bind(this)).then(function(a){return h=a.messages.length,c.render("core_message/message_area_messages_area",a)}).then(function(a,b){c.replaceNodeContents(this.messageArea.find(p.MESSAGESAREA),a,b),this._addScrollEventListener(h),this._backoffTimer.restart()}.bind(this)).fail(d.exception)},m.prototype._loadMessages=function(){if(this._isLoadingMessages)return!1;this._isLoadingMessages=!0;var b=0;return c.render("core/loading",{}).then(function(a,b){return c.prependNodeContents(this.messageArea.find(p.MESSAGES),"
"+a+"
",b),this._getMessages(this._getUserId())}.bind(this)).then(function(a){return b=a.messages.length,c.render("core_message/message_area_messages",a)}).then(function(d,e){if(this.messageArea.find(p.MESSAGES+" "+p.LOADINGICON).remove(),b>0){var f=this.messageArea.node.find(p.BLOCKTIME+":first"),g=a(d).find(p.BLOCKTIME+":first").addBack();f.html()==g.html()&&f.remove();var h=this.messageArea.find(p.MESSAGES)[0].scrollHeight;c.prependNodeContents(this.messageArea.find(p.MESSAGES),d,e);var i=this.messageArea.find(p.MESSAGES)[0].scrollHeight;this.messageArea.find(p.MESSAGES).scrollTop(i-h),this._numMessagesDisplayed+=b}this._isLoadingMessages=!1}.bind(this)).fail(d.exception)},m.prototype._loadNewMessages=function(){if(this._isLoadingMessages)return!1;if(!this._getUserId())return!1;this._isLoadingMessages=!0;var b=!1,e=this.messageArea.find(p.MESSAGES);if(0!==e.length){var f=e.scrollTop(),g=e.innerHeight(),h=e[0].scrollHeight;f+g>=h&&(b=!0)}var i=0;return this._getMessages(this._getUserId(),!0).then(function(a){var b=this.messageArea.find(p.MESSAGES);return a.messages=a.messages.filter(function(a){var c=""+a.id+a.isread,d=b.find(p.MESSAGE+'[data-id="'+c+'"]');return!d.length}),i=a.messages.length,c.render("core_message/message_area_messages",a)}.bind(this)).then(function(d,e){i>0&&(d=a(d),d.find(p.BLOCKTIME).remove(),c.appendNodeContents(this.messageArea.find(p.MESSAGES),d,e),b&&this._scrollBottom(),this._numMessagesDisplayed+=i,this._backoffTimer.restart())}.bind(this)).always(function(){this._isLoadingMessages=!1}.bind(this)).fail(d.exception)},m.prototype._getMessages=function(a,c){var e={currentuserid:this.messageArea.getCurrentUserId(),otheruserid:a,limitfrom:this._numMessagesDisplayed,limitnum:this._numMessagesToRetrieve,newest:!0};c&&(e.timefrom=this._earliestMessageTimestamp,e.limitfrom=0,e.limitnum=0);var f=b.call([{methodname:"core_message_data_for_messagearea_messages",args:e}]);return f[0].then(function(a){var b=a.messages;if(b&&b.length){var c=b[b.length-1];this._earliestMessageTimestamp?c.timecreated0?b.call(f)[f.length-1].then(function(){var b=null,c=this.messageArea.find(p.MESSAGE),d=c.last(),e=g[g.length-1];a.each(g,function(a,b){b.remove()}),d.data("id")===e.data("id")&&(b=this.messageArea.find(p.MESSAGE).last()),a.each(g,function(a,b){var c=b.data("blocktime");0===this.messageArea.find(p.MESSAGE+"[data-blocktime='"+c+"']").length&&this.messageArea.find(p.BLOCKTIME+"[data-blocktime='"+c+"']").remove()}.bind(this)),0===this.messageArea.find(p.MESSAGE).length&&this.messageArea.find(p.CONVERSATIONS+" "+p.CONTACT+"[data-userid='"+this._getUserId()+"']").remove(),this.messageArea.trigger(k.MESSAGESDELETED,[this._getUserId(),b])}.bind(this),d.exception):this.messageArea.trigger(k.MESSAGESDELETED,this._getUserId()),this._hideDeleteAction()},m.prototype._addScrollEventListener=function(a){this._scrollBottom(),this._numMessagesDisplayed=a,e.define(this.messageArea.find(p.MESSAGES),[e.events.scrollTop]),this.messageArea.onCustomEvent(e.events.scrollTop,this._loadMessages.bind(this))},m.prototype._deleteAllMessages=function(){this._confirmationModal?this._confirmationModal.show():j.get_strings([{key:"confirm"},{key:"deleteallconfirm",component:"message"}]).done(function(a){h.create({title:a[0],type:h.types.CONFIRM,body:a[1]},this.messageArea.find(p.DELETEALLMESSAGES)).done(function(a){this._confirmationModal=a,a.getRoot().on(i.yes,function(){var a=this._getUserId(),c={methodname:"core_message_delete_conversation",args:{userid:this.messageArea.getCurrentUserId(),otheruserid:a}};b.call([c])[0].then(function(){this.messageArea.find(p.MESSAGESAREA).empty(),this.messageArea.trigger(k.CONVERSATIONDELETED,a),this._hideDeleteAction()}.bind(this),d.exception)}.bind(this)),a.show()}.bind(this))}.bind(this))},m.prototype._hideDeleteAction=function(){this.messageArea.find(p.MESSAGE).removeAttr("role").removeAttr("aria-checked"),this.messageArea.find(p.MESSAGESAREA).removeClass("editing")},m.prototype._triggerCancelMessagesToDelete=function(){this.messageArea.trigger(k.CANCELDELETEMESSAGES)},m.prototype._addMessageToDom=function(){var a=b.call([{methodname:"core_message_data_for_messagearea_get_most_recent_message",args:{currentuserid:this.messageArea.getCurrentUserId(),otheruserid:this._getUserId()}}]);return a[0].then(function(a){return c.render("core_message/message_area_message",a)}).then(function(a,b){c.appendNodeContents(this.messageArea.find(p.MESSAGES),a,b),this.messageArea.find(p.SENDMESSAGETEXT).val("").trigger("input"),this._scrollBottom()}.bind(this)).fail(d.exception)},m.prototype._getUserId=function(){return this.messageArea.find(p.MESSAGES).data("userid")},m.prototype._scrollBottom=function(){var a=this.messageArea.find(p.MESSAGES);0!==a.length&&a.scrollTop(a[0].scrollHeight)},m.prototype._selectPreviousMessage=function(b,c){var d=a(b.target).closest(p.MESSAGE);do d=d.prev();while(d.length&&!d.is(p.MESSAGE));d.focus(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()},m.prototype._selectNextMessage=function(b,c){var d=a(b.target).closest(p.MESSAGE);do d=d.next();while(d.length&&!d.is(p.MESSAGE));d.focus(),c.originalEvent.preventDefault(),c.originalEvent.stopPropagation()},m.prototype._setMessaging=function(b){a(b.target).closest(p.MESSAGERESPONSE).addClass("messaging")},m.prototype._clearMessaging=function(b){a(b.target).closest(p.MESSAGERESPONSE).removeClass("messaging")},m.prototype._startDeleting=function(a){var b=new g(this.messageArea);b.chooseMessagesToDelete(),a.preventDefault()},m.prototype._isEditing=function(){return this.messageArea.find(p.MESSAGESAREA).hasClass("editing")},m.prototype._toggleMessage=function(b){if(this._isEditing()){var c=a(b.target).closest(p.MESSAGE);"true"===c.attr("aria-checked")?c.attr("aria-checked","false"):c.attr("aria-checked","true")}},m.prototype._adjustMessagesAreaHeight=function(){var a=this.messageArea.find(p.MESSAGES),b=this.messageArea.find(p.MESSAGERESPONSE),c=b.outerHeight(),d=c-o,e=n-d;a.outerHeight(e)},m.prototype._sendMessageHandler=function(a,b){b.originalEvent.preventDefault(),this._sendMessage()},m.prototype._hideMessagingArea=function(){this.messageArea.find(p.MESSAGINGAREA).removeClass("show-messages").addClass("hide-messages")},m}); \ No newline at end of file diff --git a/message/amd/src/message_area.js b/message/amd/src/message_area.js index 681712ac4419b..73bad76ce6618 100644 --- a/message/amd/src/message_area.js +++ b/message/amd/src/message_area.js @@ -29,15 +29,30 @@ define(['jquery', 'core_message/message_area_contacts', 'core_message/message_ar * Messagearea class. * * @param {String} selector The selector for the page region containing the message area. + * @param {int} pollmin + * @param {int} pollmax + * @param {int} polltimeout */ - function Messagearea(selector) { + function Messagearea(selector, pollmin, pollmax, polltimeout) { this.node = $(selector); + this.pollmin = pollmin; + this.pollmax = pollmax; + this.polltimeout = polltimeout; this._init(); } /** @type {jQuery} The jQuery node for the page region containing the message area. */ Messagearea.prototype.node = null; + /** @type {int} The minimum time to poll for messages. */ + Messagearea.prototype.pollmin = null; + + /** @type {int} The maximum time to poll for messages. */ + Messagearea.prototype.pollmax = null; + + /** @type {int} The time used once we have reached the maximum polling time. */ + Messagearea.prototype.polltimeout = null; + /** * Initialise the other objects we require. */ diff --git a/message/amd/src/message_area_messages.js b/message/amd/src/message_area_messages.js index a24ef0b0990e2..24d3c8ad60cc1 100644 --- a/message/amd/src/message_area_messages.js +++ b/message/amd/src/message_area_messages.js @@ -53,6 +53,9 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust STARTDELETEMESSAGES: "[data-action='start-delete-messages']" }; + /** @type {int} The number of milliseconds in a second. */ + var MILLISECONDSINSEC = 1000; + /** * Messages class. * @@ -81,8 +84,8 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust /** @type {int} the timestamp for the earliest visible message */ Messages.prototype._earliestMessageTimestamp = 0; - /** @type {BackOffTime} the backoff timer */ - Messages.prototype._timer = null; + /** @type {BackOffTimer} the backoff timer */ + Messages.prototype._backoffTimer = null; /** @type {Messagearea} The messaging area object. */ Messages.prototype.messageArea = null; @@ -146,12 +149,12 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust } // Create a timer to poll the server for new messages. - this._timer = new BackOffTimer(function() { - this._loadNewMessages(); - }.bind(this)); + this._backoffTimer = new BackOffTimer(this._loadNewMessages.bind(this), + BackOffTimer.getIncrementalCallback(this.messageArea.pollmin * MILLISECONDSINSEC, MILLISECONDSINSEC, + this.messageArea.pollmax * MILLISECONDSINSEC, this.messageArea.polltimeout * MILLISECONDSINSEC)); // Start the timer. - this._timer.start(); + this._backoffTimer.start(); }; /** @@ -166,7 +169,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust // We are viewing another user, or re-loading the panel, so set number of messages displayed to 0. this._numMessagesDisplayed = 0; // Stop the existing timer so we can set up the new user's messages. - this._timer.stop(); + this._backoffTimer.stop(); // Reset the earliest timestamp when we change the messages view. this._earliestMessageTimestamp = 0; @@ -203,7 +206,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust Templates.replaceNodeContents(this.messageArea.find(SELECTORS.MESSAGESAREA), html, js); this._addScrollEventListener(numberreceived); // Restart the poll timer. - this._timer.restart(); + this._backoffTimer.restart(); }.bind(this)).fail(Notification.exception); }; @@ -321,7 +324,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust // Increment the number of messages displayed. this._numMessagesDisplayed += numberreceived; // Reset the poll timer because the user may be active. - this._timer.restart(); + this._backoffTimer.restart(); } }.bind(this)).always(function() { // Mark that we are no longer busy loading data. @@ -349,7 +352,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust // If we're trying to load new messages since the message UI was // rendered. Used for ajax polling while user is on the message UI. if (fromTimestamp) { - args.createdfrom = this._earliestMessageTimestamp; + args.timefrom = this._earliestMessageTimestamp; // Remove limit and offset. We want all new messages. args.limitfrom = 0; args.limitnum = 0; @@ -381,10 +384,11 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification', 'core/cust } return data; - }.bind(this)).fail(function() { + }.bind(this)).fail(function(ex) { // Stop the timer if we received an error so that we don't keep spamming the server. - this._timer.stop(); - }.bind(this)).fail(Notification.exception); + this._backoffTimer.stop(); + Notification.exception(ex); + }.bind(this)); }; /** diff --git a/message/classes/api.php b/message/classes/api.php index 2d6150f03862f..25910dae2baf5 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -291,31 +291,29 @@ public static function get_contacts($userid, $limitfrom = 0, $limitnum = 0) { * @param int $limitfrom * @param int $limitnum * @param string $sort - * @param int $createdfrom the timestamp from which the messages were created - * @param int $createdto the time up until which the message was created + * @param int $timefrom the time from the message being sent + * @param int $timeto the time up until the message being sent * @return array */ public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0, - $sort = 'timecreated ASC', $createdfrom = 0, $createdto = 0) { + $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) { - if (!empty($createdfrom)) { + if (!empty($timefrom)) { // Check the cache to see if we even need to do a DB query. - $cache = \cache::make('core', 'message_last_created'); - $ids = [$otheruserid, $userid]; - sort($ids); - $key = implode('_', $ids); + $cache = \cache::make('core', 'message_time_last_message_between_users'); + $key = helper::get_last_message_time_created_cache_key($otheruserid, $userid); $lastcreated = $cache->get($key); // The last known message time is earlier than the one being requested so we can // just return an empty result set rather than having to query the DB. - if ($lastcreated && $lastcreated < $createdfrom) { + if ($lastcreated && $lastcreated < $timefrom) { return []; } } $arrmessages = array(); if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum, - $sort, $createdfrom, $createdto)) { + $sort, $timefrom, $timeto)) { $arrmessages = helper::create_messages($userid, $messages); } diff --git a/message/classes/helper.php b/message/classes/helper.php index 3a4c70d730d10..8ae5ea67172e5 100644 --- a/message/classes/helper.php +++ b/message/classes/helper.php @@ -43,12 +43,12 @@ class helper { * @param int $limitfrom * @param int $limitnum * @param string $sort - * @param int $createdfrom the time from which the message was created - * @param int $createdto the time up until which the message was created + * @param int $timefrom the time from the message being sent + * @param int $timeto the time up until the message being sent * @return array of messages */ public static function get_messages($userid, $otheruserid, $timedeleted = 0, $limitfrom = 0, $limitnum = 0, - $sort = 'timecreated ASC', $createdfrom = 0, $createdto = 0) { + $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) { global $DB; $messageid = $DB->sql_concat("'message_'", 'id'); @@ -77,16 +77,16 @@ public static function get_messages($userid, $otheruserid, $timedeleted = 0, $li $otheruserid, $userid, $timedeleted); $where = array(); - if (!empty($createdfrom)) { + if (!empty($timefrom)) { $where[] = 'AND timecreated >= ?'; - $params1[] = $createdfrom; - $params2[] = $createdfrom; + $params1[] = $timefrom; + $params2[] = $timefrom; } - if (!empty($createdto)) { + if (!empty($timeto)) { $where[] = 'AND timecreated <= ?'; - $params1[] = $createdto; - $params2[] = $createdto; + $params1[] = $timeto; + $params2[] = $timeto; } $sql = str_replace('%where%', implode(' ', $where), $sql); @@ -270,4 +270,17 @@ public static function togglecontact_link_params($user, $iscontact = false) { return $params; } + + /** + * Returns the cache key for the time created value of the last message between two users. + * + * @param int $userid + * @param int $user2id + * @return string + */ + public static function get_last_message_time_created_cache_key($userid, $user2id) { + $ids = [$userid, $user2id]; + sort($ids); + return implode('_', $ids); + } } diff --git a/message/classes/output/messagearea/message_area.php b/message/classes/output/messagearea/message_area.php index a3dd242de31bd..3c765917710b6 100644 --- a/message/classes/output/messagearea/message_area.php +++ b/message/classes/output/messagearea/message_area.php @@ -63,6 +63,21 @@ class message_area implements templatable, renderable { */ public $requestedconversation; + /** + * @var int The minimum time to poll for messages. + */ + public $pollmin; + + /** + * @var int The maximum time to poll for messages. + */ + public $pollmax; + + /** + * @var int The time used once we have reached the maximum polling time. + */ + public $polltimeout; + /** * Constructor. * @@ -71,13 +86,20 @@ class message_area implements templatable, renderable { * @param array $contacts * @param array|null $messages * @param bool $requestedconversation + * @param int $pollmin + * @param int $pollmax + * @param int $polltimeout */ - public function __construct($userid, $otheruserid, $contacts, $messages, $requestedconversation) { + public function __construct($userid, $otheruserid, $contacts, $messages, $requestedconversation, $pollmin, $pollmax, + $polltimeout) { $this->userid = $userid; $this->otheruserid = $otheruserid; $this->contacts = $contacts; $this->messages = $messages; $this->requestedconversation = $requestedconversation; + $this->pollmin = $pollmin; + $this->pollmax = $pollmax; + $this->polltimeout = $polltimeout; } public function export_for_template(\renderer_base $output) { @@ -89,6 +111,9 @@ public function export_for_template(\renderer_base $output) { $data->messages = $messages->export_for_template($output); $data->isconversation = true; $data->requestedconversation = $this->requestedconversation; + $data->pollmin = $this->pollmin; + $data->pollmax = $this->pollmax; + $data->polltimeout = $this->polltimeout; return $data; } diff --git a/message/classes/message_last_created_cache_source.php b/message/classes/time_last_message_between_users.php similarity index 79% rename from message/classes/message_last_created_cache_source.php rename to message/classes/time_last_message_between_users.php index ea0cd05df1b12..750a552dbbf77 100644 --- a/message/classes/message_last_created_cache_source.php +++ b/message/classes/time_last_message_between_users.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * Cache data source for the last created message between users. + * Cache data source for the time of the last message between users. * * @package core_message * @category cache @@ -23,32 +23,33 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +namespace core_message; defined('MOODLE_INTERNAL') || die(); /** - * Cache data source for the last created message between users. + * Cache data source for the time of the last message between users. * * @package core_message * @category cache * @copyright 2016 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class message_last_created_cache_source implements \cache_data_source { +class time_last_message_between_users implements \cache_data_source { - /** @var message_last_created_cache_source the singleton instance of this class. */ + /** @var time_last_message_between_users the singleton instance of this class. */ protected static $instance = null; /** * Returns an instance of the data source class that the cache can use for loading data using the other methods * specified by the cache_data_source interface. * - * @param cache_definition $definition + * @param \cache_definition $definition * @return object */ - public static function get_instance_for_cache(cache_definition $definition) { + public static function get_instance_for_cache(\cache_definition $definition) { if (is_null(self::$instance)) { - self::$instance = new message_last_created_cache_source(); + self::$instance = new time_last_message_between_users(); } return self::$instance; } @@ -62,7 +63,7 @@ public static function get_instance_for_cache(cache_definition $definition) { public function load_for_cache($key) { list($userid1, $userid2) = explode('_', $key); - $message = \core_message\api::get_most_recent_message($userid1, $userid2); + $message = api::get_most_recent_message($userid1, $userid2); if ($message) { return $message->timecreated; diff --git a/message/externallib.php b/message/externallib.php index 98b5634c9d6c7..a3b4247b746fb 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -901,7 +901,7 @@ public static function data_for_messagearea_messages_parameters() { 'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0), 'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0), 'newest' => new external_value(PARAM_BOOL, 'Newest first?', VALUE_DEFAULT, false), - 'createdfrom' => new external_value(PARAM_INT, + 'timefrom' => new external_value(PARAM_INT, 'The timestamp from which the messages were created', VALUE_DEFAULT, 0), ) ); @@ -920,7 +920,7 @@ public static function data_for_messagearea_messages_parameters() { * @since 3.2 */ public static function data_for_messagearea_messages($currentuserid, $otheruserid, $limitfrom = 0, $limitnum = 0, - $newest = false, $createdfrom = 0) { + $newest = false, $timefrom = 0) { global $CFG, $PAGE, $USER; // Check if messaging is enabled. @@ -936,7 +936,7 @@ public static function data_for_messagearea_messages($currentuserid, $otheruseri 'limitfrom' => $limitfrom, 'limitnum' => $limitnum, 'newest' => $newest, - 'createdfrom' => $createdfrom, + 'timefrom' => $timefrom, ); self::validate_parameters(self::data_for_messagearea_messages_parameters(), $params); self::validate_context($systemcontext); @@ -959,18 +959,18 @@ public static function data_for_messagearea_messages($currentuserid, $otheruseri // case those messages will be lost. // // Instead we ignore the current time in the result set to ensure that second is allowed to finish. - if (!empty($createdfrom)) { - $createdto = time() - 1; + if (!empty($timefrom)) { + $timeto = time() - 1; } else { - $createdto = 0; + $timeto = 0; } // No requesting messages from the current time, as stated above. - if ($createdfrom == time()) { - $mesages = []; + if ($timefrom == time()) { + $messages = []; } else { $messages = \core_message\api::get_messages($currentuserid, $otheruserid, $limitfrom, - $limitnum, $sort, $createdfrom, $createdto); + $limitnum, $sort, $timefrom, $timeto); } $messages = new \core_message\output\messagearea\messages($currentuserid, $otheruserid, $messages); diff --git a/message/index.php b/message/index.php index 0f834a88a0520..5789216570f84 100644 --- a/message/index.php +++ b/message/index.php @@ -127,8 +127,11 @@ $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 20, 'timecreated DESC'); } +$pollmin = !empty($CFG->messagingminpoll) ? $CFG->messagingminpoll : MESSAGE_DEFAULT_MIN_POLL_IN_SECONDS; +$pollmax = !empty($CFG->messagingmaxpoll) ? $CFG->messagingmaxpoll : MESSAGE_DEFAULT_MAX_POLL_IN_SECONDS; +$polltimeout = !empty($CFG->messagingtimeoutpoll) ? $CFG->messagingtimeoutpoll : MESSAGE_DEFAULT_TIMEOUT_POLL_IN_SECONDS; $messagearea = new \core_message\output\messagearea\message_area($user1->id, $user2->id, $conversations, $messages, - $requestedconversation); + $requestedconversation, $pollmin, $pollmax, $polltimeout); // Now the page contents. echo $OUTPUT->header(); diff --git a/message/lib.php b/message/lib.php index 34431beabbe1d..f3820c39ceaa5 100644 --- a/message/lib.php +++ b/message/lib.php @@ -76,6 +76,13 @@ */ define('MESSAGE_DEFAULT_PERMITTED', 'permitted'); +/** + * Set default values for polling. + */ +define('MESSAGE_DEFAULT_MIN_POLL_IN_SECONDS', 10); +define('MESSAGE_DEFAULT_MAX_POLL_IN_SECONDS', 2 * MINSECS); +define('MESSAGE_DEFAULT_TIMEOUT_POLL_IN_SECONDS', 5 * MINSECS); + /** * Retrieve users blocked by $user1 * diff --git a/message/templates/message_area.mustache b/message/templates/message_area.mustache index 15af327e0ba0b..8667f416daadb 100644 --- a/message/templates/message_area.mustache +++ b/message/templates/message_area.mustache @@ -32,7 +32,7 @@ {{#js}} require(['core_message/message_area'], function(Messagearea) { - new Messagearea('.messaging-area-container'); + new Messagearea('.messaging-area-container', {{pollmin}}, {{pollmax}}, {{polltimeout}}); } ); {{/js}} diff --git a/message/tests/api_test.php b/message/tests/api_test.php index 15376e049370d..9c52f86f9903d 100644 --- a/message/tests/api_test.php +++ b/message/tests/api_test.php @@ -957,7 +957,7 @@ public function is_user_enabled() { /** * Test retrieving messages by providing a minimum timecreated value. */ - public function test_get_messages_created_from_only() { + public function test_get_messages_time_from_only() { // Create some users. $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); @@ -972,7 +972,7 @@ public function test_get_messages_created_from_only() { $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3); $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4); - // Retrieve the messages. + // Retrieve the messages from $time, which should be all of them. $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time); // Confirm the message data is correct. @@ -988,7 +988,7 @@ public function test_get_messages_created_from_only() { $this->assertContains('Message 3', $message3->text); $this->assertContains('Message 4', $message4->text); - // Retrieve the messages. + // Retrieve the messages from $time + 3, which should only be the 2 last messages. $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 3); // Confirm the message data is correct. @@ -1004,7 +1004,7 @@ public function test_get_messages_created_from_only() { /** * Test retrieving messages by providing a maximum timecreated value. */ - public function test_get_messages_created_to_only() { + public function test_get_messages_time_to_only() { // Create some users. $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); @@ -1019,7 +1019,7 @@ public function test_get_messages_created_to_only() { $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3); $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4); - // Retrieve the messages. + // Retrieve the messages up until $time + 4, which should be all of them. $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 4); // Confirm the message data is correct. @@ -1035,7 +1035,7 @@ public function test_get_messages_created_to_only() { $this->assertContains('Message 3', $message3->text); $this->assertContains('Message 4', $message4->text); - // Retrieve the messages. + // Retrieve the messages up until $time + 2, which should be the first two. $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 2); // Confirm the message data is correct. @@ -1051,7 +1051,7 @@ public function test_get_messages_created_to_only() { /** * Test retrieving messages by providing a minimum and maximum timecreated value. */ - public function test_get_messages_created_from_and_to() { + public function test_get_messages_time_from_and_to() { // Create some users. $user1 = self::getDataGenerator()->create_user(); $user2 = self::getDataGenerator()->create_user(); @@ -1066,7 +1066,7 @@ public function test_get_messages_created_from_and_to() { $this->send_fake_message($user1, $user2, 'Message 3', 0, $time + 3); $this->send_fake_message($user2, $user1, 'Message 4', 0, $time + 4); - // Retrieve the messages. + // Retrieve the messages from $time + 2 up until $time + 3, which should be 2nd and 3rd message. $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 2, $time + 3); // Confirm the message data is correct. diff --git a/message/tests/externallib_test.php b/message/tests/externallib_test.php index c739b1a4e3664..3f1e38e3dd767 100644 --- a/message/tests/externallib_test.php +++ b/message/tests/externallib_test.php @@ -1982,7 +1982,7 @@ public function test_messagearea_messages() { /** * Tests retrieving messages. */ - public function test_messagearea_messages_createfrom() { + public function test_messagearea_messages_timefrom() { $this->resetAfterTest(true); // Create some users. @@ -1999,7 +1999,7 @@ public function test_messagearea_messages_createfrom() { $this->send_message($user1, $user2, 'Message 3', 0, $time - 2); $this->send_message($user2, $user1, 'Message 4', 0, $time - 1); - // Retrieve the messages. + // Retrieve the messages from $time - 3, which should be the 3 most recent messages. $result = core_message_external::data_for_messagearea_messages($user1->id, $user2->id, 0, 0, false, $time - 3); // We need to execute the return values cleaning process to simulate the web service server. From af5765b3fbe82ea8e55d8ddcc3fe220ff845fef9 Mon Sep 17 00:00:00 2001 From: Mark Nelson Date: Tue, 15 Nov 2016 14:28:15 +0800 Subject: [PATCH 3/3] MDL-56139 core_message: removed unused constants --- lib/navigationlib.php | 6 ------ message/lib.php | 23 ++--------------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/lib/navigationlib.php b/lib/navigationlib.php index 588bb99aada97..2f6c61006e4de 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -2334,9 +2334,6 @@ protected function load_for_user($user=null, $forceforcontext=false) { if ($USER->id != $user->id) { $messageargs['user2'] = $user->id; } - if ($course->id != $SITE->id) { - $messageargs['viewing'] = MESSAGE_VIEW_COURSE. $course->id; - } $url = new moodle_url('/message/index.php', $messageargs); $usernode->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages'); } @@ -4640,9 +4637,6 @@ protected function generate_user_settings($courseid, $userid, $gstitle='usercurr if ($USER->id != $user->id) { $messageargs['user2'] = $user->id; } - if ($course->id != $SITE->id) { - $messageargs['viewing'] = MESSAGE_VIEW_COURSE. $course->id; - } $url = new moodle_url('/message/index.php', $messageargs); $dashboard->add(get_string('messages', 'message'), $url, self::TYPE_SETTING, null, 'messages'); } diff --git a/message/lib.php b/message/lib.php index f3820c39ceaa5..d79fdc7042111 100644 --- a/message/lib.php +++ b/message/lib.php @@ -24,31 +24,12 @@ require_once($CFG->libdir.'/eventslib.php'); -define ('MESSAGE_SHORTLENGTH', 300); +define('MESSAGE_SHORTLENGTH', 300); -define ('MESSAGE_DISCUSSION_WIDTH',600); -define ('MESSAGE_DISCUSSION_HEIGHT',500); - -define ('MESSAGE_SHORTVIEW_LIMIT', 8);//the maximum number of messages to show on the short message history - -define('MESSAGE_HISTORY_SHORT',0); -define('MESSAGE_HISTORY_ALL',1); - -define('MESSAGE_VIEW_UNREAD_MESSAGES','unread'); -define('MESSAGE_VIEW_RECENT_CONVERSATIONS','recentconversations'); -define('MESSAGE_VIEW_RECENT_NOTIFICATIONS','recentnotifications'); -define('MESSAGE_VIEW_CONTACTS','contacts'); -define('MESSAGE_VIEW_BLOCKED','blockedusers'); -define('MESSAGE_VIEW_COURSE','course_'); -define('MESSAGE_VIEW_SEARCH','search'); +define('MESSAGE_HISTORY_ALL', 1); define('MESSAGE_SEARCH_MAX_RESULTS', 200); -define('MESSAGE_CONTACTS_PER_PAGE',10); -define('MESSAGE_MAX_COURSE_NAME_LENGTH', 30); - -define('MESSAGE_UNREAD', 'unread'); -define('MESSAGE_READ', 'read'); define('MESSAGE_TYPE_NOTIFICATION', 'notification'); define('MESSAGE_TYPE_MESSAGE', 'message');