diff --git a/lib/db/services.php b/lib/db/services.php index 52042c98ca8..795ebba85ef 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -622,6 +622,15 @@ $functions = array( 'ajax' => true, 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), ), + 'core_message_delete_conversation' => array( + 'classname' => 'core_message_external', + 'methodname' => 'delete_conversation', + 'classpath' => 'message/externallib.php', + 'description' => 'Deletes a conversation.', + 'type' => 'write', + 'capabilities' => 'moodle/site:deleteownmessage', + 'ajax' => true, + ), 'core_message_delete_message' => array( 'classname' => 'core_message_external', 'methodname' => 'delete_message', diff --git a/message/amd/src/contacts.js b/message/amd/src/contacts.js index 4ddf60bd91e..927f2937222 100644 --- a/message/amd/src/contacts.js +++ b/message/amd/src/contacts.js @@ -45,11 +45,13 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], Contacts.prototype._init = function() { this.messageArea.onCustomEvent('conversations-selected', this._viewConversations.bind(this)); this.messageArea.onCustomEvent('contacts-selected', this._viewContacts.bind(this)); - this.messageArea.onCustomEvent('messages-deleted', this._viewConversations.bind(this)); + this.messageArea.onCustomEvent('messages-deleted', this._deleteConversations.bind(this)); this.messageArea.onCustomEvent('message-send', this._viewConversationsWithUserSelected.bind(this)); this.messageArea.onCustomEvent('message-sent', this._viewConversationsWithUserSelected.bind(this)); this.messageArea.onCustomEvent('contact-removed', this._removeContact.bind(this)); this.messageArea.onCustomEvent('contact-added', this._viewContacts.bind(this)); + this.messageArea.onCustomEvent('choose-messages-to-delete', this._chooseConversationsToDelete.bind(this)); + this.messageArea.onCustomEvent('cancel-messages-deleted', this._cancelConversationsToDelete.bind(this)); this.messageArea.onDelegateEvent('click', "[data-action='view-contact-msg']", this._viewConversation.bind(this)); this.messageArea.onDelegateEvent('click', "[data-action='view-contact-profile']", this._viewContact.bind(this)); }; @@ -57,9 +59,14 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], /** * Handles viewing the list of conversations. * + * @returns {Promise} The promise resolved when the contact area has been rendered, * @private */ Contacts.prototype._viewConversations = function() { + if (this._isCurrentlyDeleting()) { + return; + } + return this._loadContactArea('core_message_data_for_messagearea_conversations'); }; @@ -72,6 +79,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], * @private */ Contacts.prototype._viewConversationsWithUserSelected = function(event, userid) { + if (this._isCurrentlyDeleting()) { + return; + } + return this._viewConversations().then(function() { this._setSelectedUser(userid); }.bind(this)); @@ -80,9 +91,14 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], /** * Handles viewing the list of contacts. * + * @returns {Promise} The promise resolved when the contact area has been rendered * @private */ Contacts.prototype._viewContacts = function() { + if (this._isCurrentlyDeleting()) { + return; + } + return this._loadContactArea('core_message_data_for_messagearea_contacts'); }; @@ -93,6 +109,10 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], * @private */ Contacts.prototype._viewConversation = function(event) { + if (this._isCurrentlyDeleting()) { + return; + } + var userid = $(event.currentTarget).data('userid'); this._setSelectedUser(userid); this.messageArea.trigger('conversation-selected', userid); @@ -105,9 +125,11 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], * @private */ Contacts.prototype._viewContact = function(event) { - var userid = $(event.currentTarget).data('userid'); - this._setSelectedUser(userid); - this.messageArea.trigger('contact-selected', userid); + if (!this._isCurrentlyDeleting()) { + var userid = $(event.currentTarget).data('userid'); + this._setSelectedUser(userid); + this.messageArea.trigger('contact-selected', userid); + } }; /** @@ -140,6 +162,72 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], }).fail(notification.exception); }; + /** + * Handles selecting conversations to delete. + * + * @private + */ + Contacts.prototype._chooseConversationsToDelete = function() { + // Only show the checkboxes for the contact if we are also deleting messages. + if (this.messageArea.find("[data-region='delete-message-checkbox']").length !== 0) { + this.messageArea.find("[data-region='delete-conversation-checkbox']").show(); + } + }; + + /** + * Handles canceling conversations to delete. + * + * @private + */ + Contacts.prototype._cancelConversationsToDelete = function() { + // Uncheck all checkboxes. + this.messageArea.find("[data-region='delete-conversation-checkbox'] input:checked").removeAttr('checked'); + // Hide the checkboxes. + this.messageArea.find("[data-region='delete-conversation-checkbox']").hide(); + }; + + /** + * Handles deleting conversations. + * + * @params {Event} event + * @params {int} The user id belonging to the messages we are deleting. + * @private + */ + Contacts.prototype._deleteConversations = function(event, userid) { + var checkboxes = this.messageArea.find("[data-region='delete-conversation-checkbox'] input:checked"); + var requests = []; + + // Go through all the checked checkboxes and prepare them for deletion. + checkboxes.each(function(id, element) { + var node = $(element); + var otheruserid = node.parents("[data-region='contact']").data('userid'); + requests.push({ + methodname: 'core_message_delete_conversation', + args: { + userid: this.messageArea.getCurrentUserId(), + otheruserid: otheruserid + } + }); + }.bind(this)); + + if (requests.length > 0) { + ajax.call(requests)[requests.length - 1].then(function() { + for (var i = 0; i <= requests.length - 1; i++) { + // Trigger conversation deleted events. + this.messageArea.trigger('conversation-deleted', requests[i].args.otheruserid); + } + }.bind(this), notification.exception); + } + + // Hide all the checkboxes. + this._cancelConversationsToDelete(); + + // Reload conversation panel. We do this regardless if a conversation was deleted or not + // as a message may have been removed which means a conversation in the list may have to + // be moved. + this._viewConversationsWithUserSelected(event, userid); + }; + /** * Handles removing a contact from the list. * @@ -164,6 +252,19 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], this.messageArea.find("[data-region='contact'][data-userid='" + userid + "']").addClass('selected'); }; + /** + * Checks if we are currently choosing conversations to delete. + * + * @return {Boolean} + */ + Contacts.prototype._isCurrentlyDeleting = function() { + if (this.messageArea.find("[data-region='delete-conversation-checkbox']:visible").length !== 0) { + return true; + } + + return false; + }; + return Contacts; } ); \ No newline at end of file diff --git a/message/amd/src/messages.js b/message/amd/src/messages.js index f558e0bbceb..c6e791fb8f4 100644 --- a/message/amd/src/messages.js +++ b/message/amd/src/messages.js @@ -43,6 +43,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], * @private */ Messages.prototype._init = function() { + this.messageArea.onCustomEvent('conversation-deleted', this._handleConversationDeleted.bind(this)); this.messageArea.onCustomEvent('conversation-selected', this._loadMessages.bind(this)); this.messageArea.onCustomEvent('message-send', this._loadMessages.bind(this)); this.messageArea.onCustomEvent('choose-messages-to-delete', this._chooseMessagesToDelete.bind(this)); @@ -174,18 +175,38 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], this.messageArea.find("[data-region='blocktime'][data-blocktime='" + blocktime + "']").remove(); } }.bind(this)); - // Simply perform the same action as canceling to delete (hide checkboxes, replace response area etc). - this._cancelMessagesToDelete(); }.bind(this), notification.exception); } + + // Hide the items responsible for deleting messages. + this._hideDeleteAction(); + + // Trigger event letting other modules know messages were deleted. + this.messageArea.trigger('messages-deleted', + this.messageArea.find("[data-region='messages']").data('userid')); + }; + + /** + * Returns the ID of the other user in the conversation. + * + * @params {Event} event + * @params {int} The user id + * @private + */ + Messages.prototype._handleConversationDeleted = function(event, userid) { + if (userid == this._getUserId()) { + // Clear the current panel. + this.messageArea.find("[data-region='messages-area']").empty(); + } }; /** - * Handles canceling deleting messages. + * Handles hiding the delete checkboxes and replacing the response area. * + * @return {Promise} JQuery promise object resolved when the template has been rendered. * @private */ - Messages.prototype._cancelMessagesToDelete = function() { + Messages.prototype._hideDeleteAction = function() { // Uncheck all checkboxes. this.messageArea.find("[data-region='delete-message-checkbox'] input:checked").removeAttr('checked'); // Hide the checkboxes. @@ -201,6 +222,18 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/notification'], } }; + /** + * Handles canceling deleting messages. + * + * @private + */ + Messages.prototype._cancelMessagesToDelete = function() { + // Hide the items responsible for deleting messages. + this._hideDeleteAction(); + // Trigger event letting other modules know message deletion was canceled. + this.messageArea.trigger('cancel-messages-deleted'); + }; + /** * Handles adding messages to the DOM. * diff --git a/message/classes/api.php b/message/classes/api.php index d487d306e6e..fa413f18d09 100644 --- a/message/classes/api.php +++ b/message/classes/api.php @@ -95,7 +95,7 @@ class api { */ public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0) { $arrmessages = array(); - if ($messages = \core_message\helper::get_messages($userid, $otheruserid, $limitfrom, $limitnum)) { + if ($messages = \core_message\helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum)) { $arrmessages = \core_message\helper::create_messages($userid, $messages); } @@ -111,7 +111,7 @@ class api { */ public static function get_most_recent_message($userid, $otheruserid) { // We want two messages here so we get an accurate 'blocktime' value. - if ($messages = \core_message\helper::get_messages($userid, $otheruserid, 0, 2, 'timecreated DESC')) { + if ($messages = \core_message\helper::get_messages($userid, $otheruserid, 0, 0, 2, 'timecreated DESC')) { // Swap the order so we now have them in historical order. $messages = array_reverse($messages); $arrmessages = \core_message\helper::create_messages($userid, $messages); @@ -159,4 +159,90 @@ class api { return new \core_message\output\profile($userid, $data); } } + + /** + * Checks if a user can delete messages they have either received or sent. + * + * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin + * but will still seem as if it was by the user) + * @return bool Returns true if a user can delete the message, false otherwise. + */ + public static function can_delete_conversation($userid) { + global $USER; + + $systemcontext = \context_system::instance(); + + // Let's check if the user is allowed to delete this message. + if (has_capability('moodle/site:deleteanymessage', $systemcontext) || + ((has_capability('moodle/site:deleteownmessage', $systemcontext) && + $USER->id == $userid))) { + return true; + } + + return false; + } + + /** + * Deletes a conversation. + * + * This function does not verify any permissions. + * + * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin + * but will still seem as if it was by the user) + * @param int $otheruserid The id of the other user in the conversation + * @return bool + */ + public static function delete_conversation($userid, $otheruserid) { + global $DB, $USER; + + // We need to update the tables to mark all messages as deleted from and to the other user. This seems worse than it + // is, that's because our DB structure splits messages into two tables (great idea, huh?) which causes code like this. + // This won't be a particularly heavily used function (at least I hope not), so let's hope MDL-36941 gets worked on + // soon for the sake of any developers' sanity when dealing with the messaging system. + $now = time(); + $sql = "UPDATE {message} + SET timeuserfromdeleted = :time + WHERE useridfrom = :userid + AND useridto = :otheruserid + AND notification = 0"; + $DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid)); + + $sql = "UPDATE {message} + SET timeusertodeleted = :time + WHERE useridto = :userid + AND useridfrom = :otheruserid + AND notification = 0"; + $DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid)); + + $sql = "UPDATE {message_read} + SET timeuserfromdeleted = :time + WHERE useridfrom = :userid + AND useridto = :otheruserid + AND notification = 0"; + $DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid)); + + $sql = "UPDATE {message_read} + SET timeusertodeleted = :time + WHERE useridto = :userid + AND useridfrom = :otheruserid + AND notification = 0"; + $DB->execute($sql, array('time' => $now, 'userid' => $userid, 'otheruserid' => $otheruserid)); + + // Now we need to trigger events for these. + if ($messages = \core_message\helper::get_messages($userid, $otheruserid, $now)) { + // Loop through and trigger a deleted event. + foreach ($messages as $message) { + $messagetable = 'message'; + if (!empty($message->timeread)) { + $messagetable = 'message_read'; + } + + // Trigger event for deleting the message. + \core\event\message_deleted::create_from_ids($message->useridfrom, $message->useridto, + $USER->id, $messagetable, $message->id)->trigger(); + } + } + + return true; + } } diff --git a/message/classes/helper.php b/message/classes/helper.php index 293efe96d29..d526d52d111 100644 --- a/message/classes/helper.php +++ b/message/classes/helper.php @@ -39,30 +39,33 @@ class helper { * * @param int $userid the current user * @param int $otheruserid the other user + * @param int $timedeleted the time the message was deleted * @param int $limitfrom * @param int $limitnum * @param string $sort * @return array of messages */ - public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0, $sort = 'timecreated ASC') { + public static function get_messages($userid, $otheruserid, $timedeleted = 0, $limitfrom = 0, $limitnum = 0, $sort = 'timecreated ASC') { global $DB; $sql = "SELECT id, useridfrom, useridto, subject, fullmessage, fullmessagehtml, fullmessageformat, smallmessage, notification, timecreated, 0 as timeread FROM {message} m - WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = 0) - OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = 0)) + WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?) + OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?)) AND notification = 0 UNION ALL SELECT id, useridfrom, useridto, subject, fullmessage, fullmessagehtml, fullmessageformat, smallmessage, notification, timecreated, timeread FROM {message_read} mr - WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = 0) - OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = 0)) + WHERE ((useridto = ? AND useridfrom = ? AND timeusertodeleted = ?) + OR (useridto = ? AND useridfrom = ? AND timeuserfromdeleted = ?)) AND notification = 0 ORDER BY $sort"; - $params = array($userid, $otheruserid, $otheruserid, $userid, - $userid, $otheruserid, $otheruserid, $userid); + $params = array($userid, $otheruserid, $timedeleted, + $otheruserid, $userid, $timedeleted, + $userid, $otheruserid, $timedeleted, + $otheruserid, $userid, $timedeleted); return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum); } diff --git a/message/externallib.php b/message/externallib.php index 3c83eee12f4..824b3b7b392 100644 --- a/message/externallib.php +++ b/message/externallib.php @@ -1354,6 +1354,84 @@ class core_message_external extends external_api { ); } + /** + * Returns description of method parameters. + * + * @return external_function_parameters + * @since 3.2 + */ + public static function delete_conversation_parameters() { + return new external_function_parameters( + array( + 'userid' => new external_value(PARAM_INT, 'The user id of who we want to delete the conversation for'), + 'otheruserid' => new external_value(PARAM_INT, 'The user id of the other user in the conversation'), + ) + ); + } + + /** + * Deletes a conversation. + * + * @param int $userid The user id of who we want to delete the conversation for + * @param int $otheruserid The user id of the other user in the conversation + * @return array + * @throws moodle_exception + * @since 3.2 + */ + public static function delete_conversation($userid, $otheruserid) { + global $CFG; + + // Check if private messaging between users is allowed. + if (empty($CFG->messaging)) { + throw new moodle_exception('disabled', 'message'); + } + + // Warnings array, it can be empty at the end but is mandatory. + $warnings = array(); + + // Validate params. + $params = array( + 'userid' => $userid, + 'otheruserid' => $otheruserid, + ); + $params = self::validate_parameters(self::delete_conversation_parameters(), $params); + + // Validate context. + $context = context_system::instance(); + self::validate_context($context); + + $user = core_user::get_user($params['userid'], '*', MUST_EXIST); + core_user::require_active_user($user); + + if (\core_message\api::can_delete_conversation($user->id)) { + $status = \core_message\api::delete_conversation($user->id, $otheruserid); + } else { + throw new moodle_exception('You do not have permission to delete messages'); + } + + $results = array( + 'status' => $status, + 'warnings' => $warnings + ); + + return $results; + } + + /** + * Returns description of method result value. + * + * @return external_description + * @since 3.2 + */ + public static function delete_conversation_returns() { + return new external_single_structure( + array( + 'status' => new external_value(PARAM_BOOL, 'True if the conversation was deleted, false otherwise'), + 'warnings' => new external_warnings() + ) + ); + } + /** * Returns description of method parameters * diff --git a/message/templates/contact.mustache b/message/templates/contact.mustache index d6e5cc1d00c..0b47aee1b7d 100644 --- a/message/templates/contact.mustache +++ b/message/templates/contact.mustache @@ -1,8 +1,11 @@
+
+ +
-
+
{{fullname}} {{#isonline}}*{{/isonline}}
{{#lastmessage}}

{{.}}

diff --git a/theme/bootstrapbase/less/moodle/message.less b/theme/bootstrapbase/less/moodle/message.less index a6fa68b43d2..81761d9172e 100644 --- a/theme/bootstrapbase/less/moodle/message.less +++ b/theme/bootstrapbase/less/moodle/message.less @@ -87,6 +87,10 @@ cursor: pointer; } + .deleteconversationcheckbox { + display: none; + } + .information { padding-left: 8px; padding-right: 8px; diff --git a/theme/bootstrapbase/style/moodle.css b/theme/bootstrapbase/style/moodle.css index 416eda714ef..6394a5400c3 100644 --- a/theme/bootstrapbase/style/moodle.css +++ b/theme/bootstrapbase/style/moodle.css @@ -5832,6 +5832,9 @@ a.ygtvspacer:hover { background-color: #CCF2FF; cursor: pointer; } +.messaging-area .contacts-area .contacts .contact .deleteconversationcheckbox { + display: none; +} .messaging-area .contacts-area .contacts .contact .information { padding-left: 8px; padding-right: 8px;