mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 00:12:56 +02:00
MDL-54687 core_message: added ability to delete conversations
This commit is contained in:
parent
16245a40b9
commit
dec0cd9998
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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>
|
||||
|
@ -87,6 +87,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.deleteconversationcheckbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.information {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user