MDL-54687 core_message: added ability to delete conversations

This commit is contained in:
Mark Nelson 2016-07-11 18:48:51 +08:00
parent 16245a40b9
commit dec0cd9998
9 changed files with 338 additions and 18 deletions

View File

@ -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',

View File

@ -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;
}
);

View File

@ -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.
*

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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
*

View File

@ -1,8 +1,11 @@
<div class="row-fluid contact {{#selected}}selected{{/selected}}" data-action="{{#lastmessage}}view-contact-msg{{/lastmessage}}{{^lastmessage}}view-contact-profile{{/lastmessage}}" data-userid="{{userid}}" data-region="contact">
<div class="span1 deleteconversationcheckbox" data-region="delete-conversation-checkbox">
<input type="checkbox"/>
</div>
<div class="span2 picture">
<img height="64" src="{{profileimageurl}}" alt="" />
</div>
<div class="span10 information">
<div class="span9 information">
<div class="name">{{fullname}} {{#isonline}}*{{/isonline}}</div>
{{#lastmessage}}
<p class="lastmessage">{{.}}</p>

View File

@ -87,6 +87,10 @@
cursor: pointer;
}
.deleteconversationcheckbox {
display: none;
}
.information {
padding-left: 8px;
padding-right: 8px;

View File

@ -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;