Merge branch 'MDL-56139_master' of git://github.com/markn86/moodle

This commit is contained in:
David Monllao
2016-11-16 11:51:18 +08:00
22 changed files with 737 additions and 56 deletions
+1
View File
@@ -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_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';
+1
View File
@@ -0,0 +1 @@
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});
+175
View File
@@ -0,0 +1,175 @@
// 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 <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(function() {
/**
* 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 BackoffTimer = function(callback, backoffFunction) {
this.callback = callback;
this.backOffFunction = backoffFunction;
};
/**
* @type {function} callback The function to execute after each tick
*/
BackoffTimer.prototype.callback = null;
/**
* @type {function} backoffFunction The function to determine what the next timeout value should be
*/
BackoffTimer.prototype.backOffFunction = null;
/**
* @type {int} time The timeout value to use
*/
BackoffTimer.prototype.time = null;
/**
* @type {numeric} timeout The timeout identifier
*/
BackoffTimer.prototype.timeout = null;
/**
* 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)
*/
BackoffTimer.prototype.generateNextTime = function() {
var newTime = this.backOffFunction(this.time);
this.time = newTime;
return newTime;
};
/**
* Stop the current timer and clear the previous time values
*
* @method reset
* @return {object} this
*/
BackoffTimer.prototype.reset = function() {
this.time = null;
this.stop();
return this;
};
/**
* Clear the current timeout, if one is set.
*
* @method stop
* @return {object} this
*/
BackoffTimer.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
*/
BackoffTimer.prototype.start = function() {
// If we haven't already started.
if (!this.timeout) {
var time = this.generateNextTime();
this.timeout = window.setTimeout(function() {
this.callback();
// 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
*/
BackoffTimer.prototype.restart = function() {
return this.reset().start();
};
/**
* 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;
});
+8
View File
@@ -301,4 +301,12 @@ $definitions = array(
'staticacceleration' => true,
'staticaccelerationsize' => 3
),
// 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' => '\core_message\time_last_message_between_users',
),
);
+9
View File
@@ -234,6 +234,15 @@ 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_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);
}
// 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;
-6
View File
@@ -2334,9 +2334,6 @@ class global_navigation extends navigation_node {
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 @@ class settings_navigation extends navigation_node {
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');
}
+1 -1
View File
@@ -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});
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});
File diff suppressed because one or more lines are too long
+16 -1
View File
@@ -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.
*/
+138 -11
View File
@@ -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;
@@ -52,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.
*
@@ -77,6 +81,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 {BackOffTimer} the backoff timer */
Messages.prototype._backoffTimer = null;
/** @type {Messagearea} The messaging area object. */
Messages.prototype.messageArea = null;
@@ -137,6 +147,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._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._backoffTimer.start();
};
/**
@@ -150,6 +168,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._backoffTimer.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 +205,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._backoffTimer.restart();
}.bind(this)).fail(Notification.exception);
};
@@ -240,28 +264,131 @@ 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._backoffTimer.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.timefrom = 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(ex) {
// Stop the timer if we received an error so that we don't keep spamming the server.
this._backoffTimer.stop();
Notification.exception(ex);
}.bind(this));
};
/**
+21 -2
View File
@@ -291,11 +291,30 @@ class api {
* @param int $limitfrom
* @param int $limitnum
* @param string $sort
* @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') {
public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
$sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
if (!empty($timefrom)) {
// Check the cache to see if we even need to do a DB query.
$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 < $timefrom) {
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, $timefrom, $timeto)) {
$arrmessages = helper::create_messages($userid, $messages);
}
+39 -5
View File
@@ -43,10 +43,12 @@ class helper {
* @param int $limitfrom
* @param int $limitnum
* @param string $sort
* @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') {
$sort = 'timecreated ASC', $timefrom = 0, $timeto = 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($timefrom)) {
$where[] = 'AND timecreated >= ?';
$params1[] = $timefrom;
$params2[] = $timefrom;
}
if (!empty($timeto)) {
$where[] = 'AND timecreated <= ?';
$params1[] = $timeto;
$params2[] = $timeto;
}
$sql = str_replace('%where%', implode(' ', $where), $sql);
$params = array_merge($params1, $params2);
return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
}
@@ -249,4 +270,17 @@ class helper {
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);
}
}
@@ -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;
@@ -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 @@ class message_area implements templatable, renderable {
$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;
}
@@ -0,0 +1,90 @@
<?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 time of the last message between users.
*
* @package core_message
* @category cache
* @copyright 2016 Ryan Wyllie <[email protected]>
* @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 time of the last message between users.
*
* @package core_message
* @category cache
* @copyright 2016 Ryan Wyllie <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class time_last_message_between_users implements \cache_data_source {
/** @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
* @return object
*/
public static function get_instance_for_cache(\cache_definition $definition) {
if (is_null(self::$instance)) {
self::$instance = new time_last_message_between_users();
}
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 = 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;
}
}
+29 -3
View File
@@ -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),
'timefrom' => 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, $timefrom = 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,
'timefrom' => $timefrom,
);
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($timefrom)) {
$timeto = time() - 1;
} else {
$timeto = 0;
}
// No requesting messages from the current time, as stated above.
if ($timefrom == time()) {
$messages = [];
} else {
$messages = \core_message\api::get_messages($currentuserid, $otheruserid, $limitfrom,
$limitnum, $sort, $timefrom, $timeto);
}
$messages = new \core_message\output\messagearea\messages($currentuserid, $otheruserid, $messages);
$renderer = $PAGE->get_renderer('core_message');
+4 -1
View File
@@ -127,8 +127,11 @@ if (!empty($user2->id)) {
$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();
+9 -21
View File
@@ -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');
@@ -76,6 +57,13 @@ define('MESSAGE_PERMITTED_MASK', 0x0c); // 1100
*/
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
*
+1 -1
View File
@@ -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}}
+125
View File
@@ -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_time_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 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.
$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 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.
$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_time_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 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.
$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 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.
$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_time_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 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.
$this->assertEquals(2, count($messages));
$message1 = $messages[0];
$message2 = $messages[1];
$this->assertContains('Message 2', $message1->text);
$this->assertContains('Message 3', $message2->text);
}
}
+40
View File
@@ -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_timefrom() {
$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 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.
$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.
*/
+2 -2
View File
@@ -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.