diff --git a/lang/en/message.php b/lang/en/message.php index 8ce4b5949ca..e3a2fb759a7 100644 --- a/lang/en/message.php +++ b/lang/en/message.php @@ -52,9 +52,11 @@ $string['defaultmessageoutputs'] = 'Notification settings'; $string['defaults'] = 'Defaults'; $string['deleteallconfirm'] = "Are you sure you would like to delete this entire conversation? This will not delete it for other conversation participants."; $string['deleteallmessages'] = "Delete all messages"; +$string['deleteallselfconfirm'] = "Are you sure you would like to delete this entire personal conversation?"; $string['deleteconversation'] = "Delete conversation"; $string['deleteselectedmessages'] = 'Delete selected messages'; $string['deleteselectedmessagesconfirm'] = 'Are you sure you would like to delete the selected messages? This will not delete them for other conversation participants.'; +$string['deleteselectedmessagesconfirmselfconversation'] = 'Are you sure you would like to delete the selected personal messages?'; $string['disableall'] = 'Disable notifications'; $string['disabled'] = 'Messaging is disabled on this site'; $string['disallowed'] = 'Disallowed'; @@ -211,6 +213,8 @@ $string['searchcombined'] = 'Search people and messages'; $string['seeall'] = 'See all'; $string['selectmessagestodelete'] = 'Select messages to delete'; $string['selectnotificationtoview'] = 'Select from the list of notifications on the side to view more details'; +$string['selfconversation'] = 'Personal space'; +$string['selfconversationdefaultmessage'] = 'Save draft messages, links, notes etc. to access later.'; $string['send'] = 'Send'; $string['sender'] = '{$a}:'; $string['sendingvia'] = 'Sending "{$a->provider}" via "{$a->processor}"'; diff --git a/lib/classes/message/manager.php b/lib/classes/message/manager.php index b35c1eda0db..321802814fb 100644 --- a/lib/classes/message/manager.php +++ b/lib/classes/message/manager.php @@ -109,6 +109,12 @@ class manager { // Get conversation type and name. We'll use this to determine which message subject to generate, depending on type. $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type, name'); + // For now Self conversations are not processed because users are aware of the messages sent by themselves, so we + // can return early. + if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF) { + return $savemessage->id; + } + // We treat individual conversations the same as any direct message with 'userfrom' and 'userto' specified. // We know the other user, so set the 'userto' field so that the event code will get access to this field. // If this was a legacy caller (eventdata->userto is set), then use that instead, as we want to use the fields specified diff --git a/lib/db/services.php b/lib/db/services.php index 95a17be3db9..0eaab66f337 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1170,6 +1170,15 @@ $functions = array( 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), 'ajax' => true ), + 'core_message_get_self_conversation' => array( + 'classname' => 'core_message_external', + 'methodname' => 'get_self_conversation', + 'classpath' => 'message/externallib.php', + 'description' => 'Retrieve a self-conversation for a user', + 'type' => 'read', + 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), + 'ajax' => true + ), 'core_message_get_messages' => array( 'classname' => 'core_message_external', 'methodname' => 'get_messages', diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 76dc1431776..76418fa72ac 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2996,9 +2996,193 @@ function xmldb_main_upgrade($oldversion) { if (!$dbman->field_exists($table, $field)) { $dbman->add_field($table, $field); } - + // Main savepoint reached. upgrade_main_savepoint(true, 2019041300.01); } + if ($oldversion < 2019041800.01) { + // STEP 1. For the existing and migrated self-conversations, set the type to the new MESSAGE_CONVERSATION_TYPE_SELF, update + // the convhash and star them. + $sql = "SELECT mcm.conversationid, mcm.userid, MAX(mcm.id) as maxid + FROM {message_conversation_members} mcm + GROUP BY mcm.conversationid, mcm.userid + HAVING COUNT(*) > 1"; + $selfconversationsrs = $DB->get_recordset_sql($sql); + $maxids = []; + foreach ($selfconversationsrs as $selfconversation) { + $DB->update_record('message_conversations', + ['id' => $selfconversation->conversationid, + 'type' => \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, + 'convhash' => \core_message\helper::get_conversation_hash([$selfconversation->userid]) + ] + ); + + // Star the existing self-conversation. + $favouriterecord = new \stdClass(); + $favouriterecord->component = 'core_message'; + $favouriterecord->itemtype = 'message_conversations'; + $favouriterecord->itemid = $selfconversation->conversationid; + $userctx = \context_user::instance($selfconversation->userid); + $favouriterecord->contextid = $userctx->id; + $favouriterecord->userid = $selfconversation->userid; + $favouriterecord->timecreated = time(); + $favouriterecord->timemodified = $favouriterecord->timecreated; + + $DB->insert_record('favourite', $favouriterecord); + + // Set the self-conversation member with maxid to remove it later. + $maxids[] = $selfconversation->maxid; + } + $selfconversationsrs->close(); + + // Remove the repeated member with the higher id for all the existing self-conversations. + if (!empty($maxids)) { + list($insql, $inparams) = $DB->get_in_or_equal($maxids); + $DB->delete_records_select('message_conversation_members', "id $insql", $inparams); + } + + // STEP 2. Migrate existing self-conversation relying on old message tables, setting the type to the new + // MESSAGE_CONVERSATION_TYPE_SELF and the convhash to the proper one. Star them also. + + // On the messaging legacy tables, self-conversations are only present in the 'message_read' table, so we don't need to + // check the content in the 'message' table. + $select = 'useridfrom = useridto AND notification = 0'; + $legacyselfmessagesrs = $DB->get_recordset_select('message_read', $select); + foreach ($legacyselfmessagesrs as $message) { + // Get the self-conversation or create and star it if doesn't exist. + $conditions = [ + 'type' => \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, + 'convhash' => \core_message\helper::get_conversation_hash([$message->useridfrom]) + ]; + $selfconversation = $DB->get_record('message_conversations', $conditions); + if (empty($selfconversation)) { + // Create the self-conversation. + $selfconversation = new \stdClass(); + $selfconversation->type = \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF; + $selfconversation->convhash = \core_message\helper::get_conversation_hash([$message->useridfrom]); + $selfconversation->enabled = 1; + $selfconversation->timecreated = time(); + $selfconversation->timemodified = $selfconversation->timecreated; + + $selfconversation->id = $DB->insert_record('message_conversations', $selfconversation); + + // Add user to this self-conversation. + $member = new \stdClass(); + $member->conversationid = $selfconversation->id; + $member->userid = $message->useridfrom; + $member->timecreated = time(); + + $member->id = $DB->insert_record('message_conversation_members', $member); + + // Star the self-conversation. + $favouriterecord = new \stdClass(); + $favouriterecord->component = 'core_message'; + $favouriterecord->itemtype = 'message_conversations'; + $favouriterecord->itemid = $selfconversation->id; + $userctx = \context_user::instance($message->useridfrom); + $favouriterecord->contextid = $userctx->id; + $favouriterecord->userid = $message->useridfrom; + $favouriterecord->timecreated = time(); + $favouriterecord->timemodified = $favouriterecord->timecreated; + + $DB->insert_record('favourite', $favouriterecord); + } + + // Create the object we will be inserting into the database. + $tabledata = new \stdClass(); + $tabledata->useridfrom = $message->useridfrom; + $tabledata->conversationid = $selfconversation->id; + $tabledata->subject = $message->subject; + $tabledata->fullmessage = $message->fullmessage; + $tabledata->fullmessageformat = $message->fullmessageformat ?? FORMAT_MOODLE; + $tabledata->fullmessagehtml = $message->fullmessagehtml; + $tabledata->smallmessage = $message->smallmessage; + $tabledata->timecreated = $message->timecreated; + + $messageid = $DB->insert_record('messages', $tabledata); + + // Check if we need to mark this message as deleted (self-conversations add this information on the + // timeuserfromdeleted field. + if ($message->timeuserfromdeleted) { + $mua = new \stdClass(); + $mua->userid = $message->useridfrom; + $mua->messageid = $messageid; + $mua->action = \core_message\api::MESSAGE_ACTION_DELETED; + $mua->timecreated = $message->timeuserfromdeleted; + + $DB->insert_record('message_user_actions', $mua); + } + + // Mark this message as read. + $mua = new \stdClass(); + $mua->userid = $message->useridto; + $mua->messageid = $messageid; + $mua->action = \core_message\api::MESSAGE_ACTION_READ; + $mua->timecreated = $message->timeread; + + $DB->insert_record('message_user_actions', $mua); + } + $legacyselfmessagesrs->close(); + + // We can now delete the records from legacy table because the self-conversations have been migrated from the legacy tables. + $DB->delete_records_select('message_read', $select); + + // STEP 3. For existing users without self-conversations, create and star it. + + // Get all the users without a self-conversation. + $sql = "SELECT u.id + FROM {user} u + WHERE u.id NOT IN (SELECT mcm.userid + FROM {message_conversation_members} mcm + INNER JOIN mdl_message_conversations mc + ON mc.id = mcm.conversationid AND mc.type = ? + )"; + $useridsrs = $DB->get_recordset_sql($sql, [\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF]); + // Create the self-conversation for all these users. + foreach ($useridsrs as $user) { + $conditions = [ + 'type' => \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, + 'convhash' => \core_message\helper::get_conversation_hash([$user->id]) + ]; + $selfconversation = $DB->get_record('message_conversations', $conditions); + if (empty($selfconversation)) { + // Create the self-conversation. + $selfconversation = new \stdClass(); + $selfconversation->type = \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF; + $selfconversation->convhash = \core_message\helper::get_conversation_hash([$user->id]); + $selfconversation->enabled = 1; + $selfconversation->timecreated = time(); + $selfconversation->timemodified = $selfconversation->timecreated; + + $selfconversation->id = $DB->insert_record('message_conversations', $selfconversation); + + // Add user to this self-conversation. + $member = new \stdClass(); + $member->conversationid = $selfconversation->id; + $member->userid = $user->id; + $member->timecreated = time(); + + $member->id = $DB->insert_record('message_conversation_members', $member); + + // Star the self-conversation. + $favouriterecord = new \stdClass(); + $favouriterecord->component = 'core_message'; + $favouriterecord->itemtype = 'message_conversations'; + $favouriterecord->itemid = $selfconversation->id; + $userctx = \context_user::instance($user->id); + $favouriterecord->contextid = $userctx->id; + $favouriterecord->userid = $user->id; + $favouriterecord->timecreated = time(); + $favouriterecord->timemodified = $favouriterecord->timecreated; + + $DB->insert_record('favourite', $favouriterecord); + } + } + $useridsrs->close(); + + // Main savepoint reached. + upgrade_main_savepoint(true, 2019041800.01); + } + return true; } diff --git a/lib/messagelib.php b/lib/messagelib.php index 2f3b4174c0b..1dcc437c2e1 100644 --- a/lib/messagelib.php +++ b/lib/messagelib.php @@ -118,18 +118,30 @@ function message_send(\core\message\message $eventdata) { return false; } - if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id, - $eventdata->userto->id])) { - $conversation = \core_message\api::create_conversation( - \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, - [ - $eventdata->userfrom->id, - $eventdata->userto->id - ] - ); + if ($eventdata->userfrom->id == $eventdata->userto->id) { + // It's a self conversation. + $conversation = \core_message\api::get_self_conversation($eventdata->userfrom->id); + if (empty($conversation)) { + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, + [$eventdata->userfrom->id] + ); + } + } else { + if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id, + $eventdata->userto->id])) { + // It's a private conversation between users. + $conversation = \core_message\api::create_conversation( + \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, + [ + $eventdata->userfrom->id, + $eventdata->userto->id + ] + ); + } } // We either have found a conversation, or created one. - $conversationid = $conversationid ? $conversationid : $conversation->id; + $conversationid = !empty($conversationid) ? $conversationid : $conversation->id; $eventdata->convid = $conversationid; } diff --git a/lib/outputrenderers.php b/lib/outputrenderers.php index 27ee12d0650..4845832cde7 100644 --- a/lib/outputrenderers.php +++ b/lib/outputrenderers.php @@ -4090,11 +4090,7 @@ EOD; $imagedata = $this->user_picture($user, array('size' => 100)); // Check to see if we should be displaying a message button. - if (!empty($CFG->messaging) && $USER->id != $user->id && has_capability('moodle/site:sendmessage', $context)) { - $iscontact = \core_message\api::is_contact($USER->id, $user->id); - $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts'; - $contacturlaction = $iscontact ? 'removecontact' : 'addcontact'; - $contactimage = $iscontact ? 'removecontact' : 'addcontact'; + if (!empty($CFG->messaging) && has_capability('moodle/site:sendmessage', $context)) { $userbuttons = array( 'messages' => array( 'buttontype' => 'message', @@ -4103,22 +4099,29 @@ EOD; 'image' => 'message', 'linkattributes' => \core_message\helper::messageuser_link_params($user->id), 'page' => $this->page - ), - 'togglecontact' => array( - 'buttontype' => 'togglecontact', - 'title' => get_string($contacttitle, 'message'), - 'url' => new moodle_url('/message/index.php', array( - 'user1' => $USER->id, - 'user2' => $user->id, - $contacturlaction => $user->id, - 'sesskey' => sesskey()) - ), - 'image' => $contactimage, - 'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact), - 'page' => $this->page - ), + ) ); + if ($USER->id != $user->id) { + $iscontact = \core_message\api::is_contact($USER->id, $user->id); + $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts'; + $contacturlaction = $iscontact ? 'removecontact' : 'addcontact'; + $contactimage = $iscontact ? 'removecontact' : 'addcontact'; + $userbuttons['togglecontact'] = array( + 'buttontype' => 'togglecontact', + 'title' => get_string($contacttitle, 'message'), + 'url' => new moodle_url('/message/index.php', array( + 'user1' => $USER->id, + 'user2' => $user->id, + $contacturlaction => $user->id, + 'sesskey' => sesskey()) + ), + 'image' => $contactimage, + 'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact), + 'page' => $this->page + ); + } + $this->page->requires->string_for_js('changesmadereallygoaway', 'moodle'); } } else { diff --git a/lib/tests/messagelib_test.php b/lib/tests/messagelib_test.php index 489a4852978..e4ee895bd77 100644 --- a/lib/tests/messagelib_test.php +++ b/lib/tests/messagelib_test.php @@ -819,6 +819,59 @@ class core_messagelib_testcase extends advanced_testcase { $sink->clear(); } + /** + * Tests calling message_send() with $eventdata representing a message to a self-conversation. + * + * This test will verify: + * - that the 'messages' record is created. + * - that the processors is not called (for now self-conversations are not processed). + * - the a single event will be generated - 'message_sent' + * + * Note: We won't redirect/capture messages in this test because doing so causes message_send() to return early, before + * processors and events code is called. We need to test this code here, as we generally redirect messages elsewhere and we + * need to be sure this is covered. + */ + public function test_message_send_to_self_conversation() { + global $DB; + $this->preventResetByRollback(); + $this->resetAfterTest(); + + // Create some users and a conversation between them. + $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1)); + set_config('allowedemaildomains', 'example.com'); + $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, + [$user1->id]); + + // Generate the message. + $message = new \core\message\message(); + $message->courseid = 1; + $message->component = 'moodle'; + $message->name = 'instantmessage'; + $message->userfrom = $user1; + $message->convid = $conversation->id; + $message->subject = 'message subject 1'; + $message->fullmessage = 'message body'; + $message->fullmessageformat = FORMAT_MARKDOWN; + $message->fullmessagehtml = '
message body
'; + $message->smallmessage = 'small message'; + $message->notification = '0'; + + // Content specific to the email processor. + $content = array('*' => array('header' => ' test ', 'footer' => ' test ')); + $message->set_additional_content('email', $content); + + // Ensure we're going to hit the email processor for this user. + $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'"); + set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user1); + + // Now, send a message and verify the message processors are empty (self-conversations are not processed for now). + $sink = $this->redirectEmails(); + $messageid = message_send($message); + $emails = $sink->get_messages(); + $this->assertCount(0, $emails); + $sink->clear(); + } + /** * Tests calling message_send() with $eventdata representing a message to an group conversation. * diff --git a/lib/upgrade.txt b/lib/upgrade.txt index d0baed6779b..5207a6ecf24 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -31,6 +31,14 @@ attribute on forms to avoid collisions in forms loaded in AJAX requests. * It is possible to pass additional conditions to get_courses_search(); core_course_category::search_courses() now allows to search only among courses with completion enabled. * Add support for a new xxx_after_require_login callback +* A new conversation type has been created for self-conversations. During the upgrading process: + - Firstly, the existing self-conversations will be starred and migrated to the new type, removing the duplicated members in the + message_conversation_members table. + - Secondly, the legacy self conversations will be migrated from the legacy 'message_read' table. They will be created using the + new conversation type and will be favourited. + - Finally, the self-conversations for all remaining users without them will be created and starred. +Besides, from now, a self-conversation will be created and starred by default to all the new users (even when $CFG->messaging +is disabled). === 3.6 === diff --git a/message/amd/build/message_drawer_view_conversation.min.js b/message/amd/build/message_drawer_view_conversation.min.js index 6d5e118dbe3..21837f57b83 100644 --- a/message/amd/build/message_drawer_view_conversation.min.js +++ b/message/amd/build/message_drawer_view_conversation.min.js @@ -1 +1 @@ -define(["jquery","core/auto_rows","core/backoff_timer","core/custom_interaction_events","core/notification","core/pubsub","core/str","core_message/message_repository","core_message/message_drawer_events","core_message/message_drawer_view_conversation_constants","core_message/message_drawer_view_conversation_patcher","core_message/message_drawer_view_conversation_renderer","core_message/message_drawer_view_conversation_state_manager","core_message/message_drawer_router","core_message/message_drawer_routes"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var p={},q=null,r=!1,s=0,t=null,u=!0,v=!1,w=null,x=[],y=j.NEWEST_MESSAGES_FIRST,z=j.LOAD_MESSAGE_LIMIT,A=j.INITIAL_NEW_MESSAGE_POLL_TIMEOUT,B=j.SELECTORS,C=j.CONVERSATION_TYPES,D=function(){if(!q||q.type!=C.PRIVATE)return null;var a=q.loggedInUserId,b=Object.keys(q.members).filter(function(b){return a!=b});return b.length?b[0]:null},E=function(a){return Object.keys(p).reduce(function(b,c){if(!b){var d=p[c].state;d.type==C.PRIVATE&&a in d.members&&(b=d.id)}return b},null)},F=function(a){return{id:parseInt(a.attr("data-user-id"),10),fullname:null,profileimageurl:null,profileimageurlsmall:null,isonline:null,showonlinestatus:null,isblocked:null,iscontact:null,isdeleted:null,canmessage:null,requirescontact:null,contactrequests:[]}},G=function(){return s},H=function(a){s=a,p[q.id].messagesOffset=a},I=function(){return r},J=function(a){r=a,p[q.id].loadedAllMessages=a},K=function(a){return a.find(B.MESSAGES_CONTAINER)},L=function(b){return{id:b.id,name:b.name,subname:b.subname,imageUrl:b.imageUrl,isFavourite:b.isFavourite,isMuted:b.isMuted,type:b.type,totalMemberCount:b.totalMemberCount,loggedInUserId:b.loggedInUserId,messages:b.messages.map(function(b){return a.extend({},b)}),members:Object.keys(b.members).reduce(function(c,d){return c[d]=a.extend({},b.members[d]),c[d].contactrequests=b.members[d].contactrequests.map(function(b){return a.extend({},b)}),c},{})}},M=function(a,b){var c=a.id,d=m.setLoadingMembers(q,!0);return d=m.setLoadingMessages(d,!0),w(d).then(function(){return h.getMemberInfo(c,[b],!0,!0)}).then(function(a){if(a.length)return a[0];throw new Error("Unable to load other user profile")}).then(function(b){var c=m.addMembers(q,[b,a]);return c=m.setLoadingMembers(c,!1),c=m.setLoadingMessages(c,!1),c=m.setName(c,b.fullname),c=m.setType(c,1),c=m.setImageUrl(c,b.profileimageurl),c=m.setTotalMemberCount(c,2),w(c).then(function(){return b})})["catch"](function(a){var b=m.setLoadingMembers(q,!1);w(b),e.exception(a)})},N=function(a,b){var c=a.members.filter(function(a){return a.id!=b}),d=c.length?c[0]:null,e=a.name,f=a.imageurl;a.type==C.PRIVATE&&(e=e||d?d.fullname:"",f=f||d?d.profileimageurl:"");var g=m.addMembers(q,a.members);return g=m.setName(g,e),g=m.setSubname(g,a.subname),g=m.setType(g,a.type),g=m.setImageUrl(g,f),g=m.setTotalMemberCount(g,a.membercount),g=m.setIsFavourite(g,a.isfavourite),g=m.setIsMuted(g,a.ismuted),g=m.addMessages(g,a.messages)},O=function(a,b,c,d,f){var g=b.id,i=m.setLoadingMembers(q,!0);return i=m.setLoadingMessages(i,!0),w(i).then(function(){return h.getConversation(g,a,!0,!0,0,0,c+1,d,f)}).then(function(a){return a.messages.length>c?a.messages=a.messages.slice(1):J(!0),H(d+c),a}).then(function(a){var c=a.members.filter(function(a){return a.id==b.id});c.length<1&&(a.members=a.members.concat([b]));var d=N(a,b.id);return d=m.setLoadingMembers(d,!1),d=m.setLoadingMessages(d,!1),w(d).then(function(){return a})}).then(function(){return S(a)})["catch"](function(a){var b=m.setLoadingMembers(q,!1);b=m.setLoadingMessages(b,!1),w(b),e.exception(a)})},P=function(a,b,c,d){var f=a.members.filter(function(a){return a.id==b.id});f.length<1&&(a.members=a.members.concat([b]));var g=N(a,b.id);g=m.setLoadingMembers(g,!1),g=m.setLoadingMessages(g,!0);var h=a.messages.length;return w(g).then(function(){if(h