mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 05:58:34 +01:00
MDL-56139 message: ajax poll for new messages in message area
This commit is contained in:
parent
b4d6669dd0
commit
fb1469d84f
@ -52,6 +52,7 @@ $string['cachedef_suspended_userids'] = 'List of suspended users per course';
|
||||
$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';
|
||||
|
1
lib/amd/build/backoff_timer.min.js
vendored
Normal file
1
lib/amd/build/backoff_timer.min.js
vendored
Normal file
@ -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});
|
206
lib/amd/src/backoff_timer.js
Normal file
206
lib/amd/src/backoff_timer.js
Normal file
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* 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 <ryan@moodle.com>
|
||||
* @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;
|
||||
});
|
@ -301,4 +301,13 @@ $definitions = array(
|
||||
'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'
|
||||
),
|
||||
);
|
||||
|
@ -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;
|
||||
|
File diff suppressed because one or more lines are too long
@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -291,11 +291,32 @@ class api {
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
@ -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 @@ class helper {
|
||||
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 @@ class helper {
|
||||
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);
|
||||
}
|
||||
|
89
message/classes/message_last_created_cache_source.php
Normal file
89
message/classes/message_last_created_cache_source.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Cache data source for the last created message between users.
|
||||
*
|
||||
* @package core_message
|
||||
* @category cache
|
||||
* @copyright 2016 Ryan Wyllie <ryan@moodle.com>
|
||||
* @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 <ryan@moodle.com>
|
||||
* @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;
|
||||
}
|
||||
}
|
@ -107,6 +107,7 @@ class message implements templatable, renderable {
|
||||
$message->position = 'right';
|
||||
}
|
||||
$message->timesent = userdate($this->timecreated, get_string('strftimetime'));
|
||||
$message->timecreated = $this->timecreated;
|
||||
$message->isread = !empty($this->timeread) ? 1 : 0;
|
||||
|
||||
return $message;
|
||||
|
@ -515,6 +515,7 @@ class core_message_external extends external_api {
|
||||
'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 @@ class core_message_external extends external_api {
|
||||
'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 @@ class core_message_external extends external_api {
|
||||
* @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 @@ class core_message_external extends external_api {
|
||||
'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 @@ class core_message_external extends external_api {
|
||||
} 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');
|
||||
|
@ -953,4 +953,129 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
@ -1979,6 +1979,46 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
|
||||
$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.
|
||||
*/
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user