MDL-64715 message: add support for self conversations
authorSara Arjona <sara@moodle.com>
Wed, 6 Mar 2019 20:26:29 +0000 (21:26 +0100)
committerSara Arjona <sara@moodle.com>
Mon, 15 Apr 2019 18:17:54 +0000 (20:17 +0200)
Added new MESSAGE_CONVERSATION_TYPE_SELF type for self-conversations
and upgraded legacy self-conversations to the new type, removing
repeated members in the message_conversation_members table.
Besides, from now, a self-conversation will be created by default for
all the existing users.

All the self-conversations have been also starred and a default message
will be displayed always to explain how to use them.

33 files changed:
lang/en/message.php
lib/classes/message/manager.php
lib/db/services.php
lib/db/upgrade.php
lib/messagelib.php
lib/tests/messagelib_test.php
lib/upgrade.txt
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation_constants.min.js
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_renderer.min.js
message/amd/build/message_drawer_view_overview.min.js
message/amd/build/message_drawer_view_overview_section.min.js
message/amd/build/message_repository.min.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_conversation_constants.js
message/amd/src/message_drawer_view_conversation_patcher.js
message/amd/src/message_drawer_view_conversation_renderer.js
message/amd/src/message_drawer_view_overview.js
message/amd/src/message_drawer_view_overview_section.js
message/amd/src/message_repository.js
message/classes/api.php
message/classes/privacy/provider.php
message/classes/task/migrate_message_data.php
message/externallib.php
message/templates/message_drawer_view_conversation_body.mustache
message/templates/message_drawer_view_conversation_header_content_type_self.mustache [new file with mode: 0644]
message/tests/api_test.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
message/upgrade.txt
user/lib.php
version.php

index 8ce4b59..e3a2fb7 100644 (file)
@@ -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}"';
index b35c1ed..3218028 100644 (file)
@@ -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
index 95a17be..0eaab66 100644 (file)
@@ -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',
index 8bf8063..719dd5c 100644 (file)
@@ -2988,5 +2988,189 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2019041000.02);
     }
 
+    if ($oldversion < 2019041300.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, 2019041300.01);
+    }
+
     return true;
 }
index 2f3b417..1dcc437 100644 (file)
@@ -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;
     }
 
index 489a485..e4ee895 100644 (file)
@@ -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   = '<p>message body</p>';
+        $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.
      *
index f1bbece..da93514 100644 (file)
@@ -30,6 +30,14 @@ attribute on forms to avoid collisions in forms loaded in AJAX requests.
   in this category. To work with list of courses use API methods in core_course_category and also 'course' form element.
 * 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.
+* 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 ===
 
index 6d5e118..f681f92 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation.min.js and b/message/amd/build/message_drawer_view_conversation.min.js differ
index a45db35..464ad1e 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation_constants.min.js and b/message/amd/build/message_drawer_view_conversation_constants.min.js differ
index 7ec4cfd..c818b03 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation_patcher.min.js and b/message/amd/build/message_drawer_view_conversation_patcher.min.js differ
index b23ddc6..044ff9e 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation_renderer.min.js and b/message/amd/build/message_drawer_view_conversation_renderer.min.js differ
index 7ce1ee6..aa0cded 100644 (file)
Binary files a/message/amd/build/message_drawer_view_overview.min.js and b/message/amd/build/message_drawer_view_overview.min.js differ
index 42e5adf..7717264 100644 (file)
Binary files a/message/amd/build/message_drawer_view_overview_section.min.js and b/message/amd/build/message_drawer_view_overview_section.min.js differ
index 999ba17..4b0e943 100644 (file)
Binary files a/message/amd/build/message_repository.min.js and b/message/amd/build/message_repository.min.js differ
index 8b8949b..6081051 100644 (file)
@@ -120,11 +120,16 @@ function(
      * @return {Number} Userid.
      */
     var getOtherUserId = function() {
-        if (!viewState || viewState.type != CONVERSATION_TYPES.PRIVATE) {
+        if (!viewState || (viewState.type != CONVERSATION_TYPES.PRIVATE && viewState.type != CONVERSATION_TYPES.SELF)) {
             return null;
         }
 
         var loggedInUserId = viewState.loggedInUserId;
+        if (viewState.type == CONVERSATION_TYPES.SELF) {
+            // It's a self-conversation, so the other user is the one logged in.
+            return loggedInUserId;
+        }
+
         var otherUserIds = Object.keys(viewState.members).filter(function(userId) {
             return loggedInUserId != userId;
         });
@@ -144,7 +149,7 @@ function(
             if (!carry) {
                 var state = stateCache[id].state;
 
-                if (state.type == CONVERSATION_TYPES.PRIVATE) {
+                if (state.type == CONVERSATION_TYPES.PRIVATE || state.type == CONVERSATION_TYPES.SELF) {
                     if (userId in state.members) {
                         // We've found a cached conversation for this user!
                         carry = state.id;
@@ -287,7 +292,7 @@ function(
                 newState = StateManager.setLoadingMembers(newState, false);
                 newState = StateManager.setLoadingMessages(newState, false);
                 newState = StateManager.setName(newState, profile.fullname);
-                newState = StateManager.setType(newState, 1);
+                newState = StateManager.setType(newState, CONVERSATION_TYPES.PRIVATE);
                 newState = StateManager.setImageUrl(newState, profile.profileimageurl);
                 newState = StateManager.setTotalMemberCount(newState, 2);
                 return render(newState)
@@ -302,6 +307,50 @@ function(
             });
     };
 
+    /**
+     * Load up an empty self-conversation for the logged in user.
+     * Sets all of the conversation details based on the current user.
+     *
+     * A conversation isn't created until the user sends the first message.
+     *
+     * @param  {Object} loggedInUserProfile The logged in user profile.
+     * @return {Object} Profile returned from repository.
+     */
+    var loadEmptySelfConversation = function(loggedInUserProfile) {
+        var loggedInUserId = loggedInUserProfile.id;
+        var newState = StateManager.setLoadingMembers(viewState, true);
+        newState = StateManager.setLoadingMessages(newState, true);
+        return render(newState)
+            .then(function() {
+                return Repository.getMemberInfo(loggedInUserId, [loggedInUserId], true, true);
+            })
+            .then(function(profiles) {
+                if (profiles.length) {
+                    return profiles[0];
+                } else {
+                    throw new Error('Unable to load other user profile');
+                }
+            })
+            .then(function(profile) {
+                var newState = StateManager.addMembers(viewState, [profile, loggedInUserProfile]);
+                newState = StateManager.setLoadingMembers(newState, false);
+                newState = StateManager.setLoadingMessages(newState, false);
+                newState = StateManager.setName(newState, profile.fullname);
+                newState = StateManager.setType(newState, CONVERSATION_TYPES.SELF);
+                newState = StateManager.setImageUrl(newState, profile.profileimageurl);
+                newState = StateManager.setTotalMemberCount(newState, 1);
+                return render(newState)
+                    .then(function() {
+                        return profile;
+                    });
+            })
+            .catch(function(error) {
+                var newState = StateManager.setLoadingMembers(viewState, false);
+                render(newState);
+                Notification.exception(error);
+            });
+    };
+
     /**
      * Create a new state from a conversation object.
      *
@@ -310,14 +359,21 @@ function(
      * @return {Object} new state.
      */
     var updateStateFromConversation = function(conversation, loggedInUserId) {
-        var otherUsers = conversation.members.filter(function(member) {
-            return member.id != loggedInUserId;
-        });
-        var otherUser = otherUsers.length ? otherUsers[0] : null;
+        var otherUser = null;
+        if (conversation.type == CONVERSATION_TYPES.PRIVATE) {
+            // For private conversations, remove current logged in user from the members list to get the other user.
+            var otherUsers = conversation.members.filter(function(member) {
+                return member.id != loggedInUserId;
+            });
+            otherUser = otherUsers.length ? otherUsers[0] : null;
+        } else if (conversation.type == CONVERSATION_TYPES.SELF) {
+            // Self-conversations have only one member.
+            otherUser = conversation.members[0];
+        }
+
         var name = conversation.name;
         var imageUrl = conversation.imageurl;
-
-        if (conversation.type == CONVERSATION_TYPES.PRIVATE) {
+        if (conversation.type == CONVERSATION_TYPES.PRIVATE || conversation.type == CONVERSATION_TYPES.SELF) {
             name = name || otherUser ? otherUser.fullname : '';
             imageUrl = imageUrl || otherUser ? otherUser.profileimageurl : '';
         }
@@ -921,6 +977,7 @@ function(
                 newState = StateManager.setPendingDeleteConversation(newState, false);
                 newState = StateManager.setLoadingConfirmAction(newState, false);
                 PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id);
+
                 return render(newState);
             });
     };
@@ -1019,7 +1076,8 @@ function(
         var newConversationId = null;
         return render(newState)
             .then(function() {
-                if (!conversationId && viewState.type == CONVERSATION_TYPES.PRIVATE) {
+                if (!conversationId &&
+                    (viewState.type == CONVERSATION_TYPES.PRIVATE || viewState.type == CONVERSATION_TYPES.SELF)) {
                     // If it's a new private conversation then we need to use the old
                     // web service function to create the conversation.
                     var otherUserId = getOtherUserId();
@@ -1471,7 +1529,7 @@ function(
     };
 
     /**
-     * Load a new empty private conversation between two users.
+     * Load a new empty private conversation between two users or self-conversation.
      *
      * @param  {Object} body Conversation body container element.
      * @param  {Object} loggedInUserProfile The logged in user's profile.
@@ -1481,28 +1539,50 @@ function(
     var resetNoConversation = function(body, loggedInUserProfile, otherUserId) {
         // Always reset the state back to the initial state so that the
         // state manager and patcher can work correctly.
-        return resetState(body, null, loggedInUserProfile)
-            .then(function() {
-                return Repository.getConversationBetweenUsers(
-                        loggedInUserProfile.id,
-                        otherUserId,
-                        true,
-                        true,
-                        0,
-                        0,
-                        LOAD_MESSAGE_LIMIT,
-                        0,
-                        NEWEST_FIRST
-                    )
-                    .then(function(conversation) {
-                        // Looks like we have a conversation after all! Let's use that.
-                        return resetByConversation(body, conversation, loggedInUserProfile);
-                    })
-                    .catch(function() {
-                        // Can't find a conversation. Oh well. Just load up a blank one.
-                        return loadEmptyPrivateConversation(loggedInUserProfile, otherUserId);
-                    });
-            });
+        if (loggedInUserProfile.id != otherUserId) {
+            // This is a private conversation between two users.
+            return resetState(body, null, loggedInUserProfile)
+                .then(function() {
+                    return Repository.getConversationBetweenUsers(
+                            loggedInUserProfile.id,
+                            otherUserId,
+                            true,
+                            true,
+                            0,
+                            0,
+                            LOAD_MESSAGE_LIMIT,
+                            0,
+                            NEWEST_FIRST
+                        )
+                        .then(function(conversation) {
+                            // Looks like we have a conversation after all! Let's use that.
+                            return resetByConversation(body, conversation, loggedInUserProfile);
+                        })
+                        .catch(function() {
+                            // Can't find a conversation. Oh well. Just load up a blank one.
+                            return loadEmptyPrivateConversation(loggedInUserProfile, otherUserId);
+                        });
+                });
+        } else {
+            // This is a self-conversation.
+            return resetState(body, null, loggedInUserProfile)
+                .then(function() {
+                    return Repository.getSelfConversation(
+                            loggedInUserProfile.id,
+                            LOAD_MESSAGE_LIMIT,
+                            0,
+                            NEWEST_FIRST
+                        )
+                        .then(function(conversation) {
+                            // Looks like we have a conversation after all! Let's use that.
+                            return resetByConversation(body, conversation, loggedInUserProfile);
+                        })
+                        .catch(function() {
+                            // Can't find a conversation. Oh well. Just load up a blank one.
+                            return loadEmptySelfConversation(loggedInUserProfile);
+                        });
+                });
+        }
     };
 
     /**
index 3bd84c8..5c1e4f0 100644 (file)
@@ -78,6 +78,7 @@ define([], function() {
         MORE_MESSAGES_LOADING_ICON_CONTAINER: '[data-region="more-messages-loading-icon-container"]',
         MUTED_ICON_CONTAINER: '[data-region="muted-icon-container"]',
         PLACEHOLDER_CONTAINER: '[data-region="placeholder-container"]',
+        SELF_CONVERSATION_MESSAGE_CONTAINER: '[data-region="self-conversation-message-container"]',
         SEND_MESSAGE_BUTTON: '[data-action="send-message"]',
         SEND_MESSAGE_ICON_CONTAINER: '[data-region="send-icon-container"]',
         TEXT: '[data-region="text"]',
@@ -88,20 +89,32 @@ define([], function() {
         HEADER_PRIVATE: 'core_message/message_drawer_view_conversation_header_content_type_private',
         HEADER_PRIVATE_NO_CONTROLS: 'core_message/message_drawer_view_conversation_header_content_type_private_no_controls',
         HEADER_PUBLIC: 'core_message/message_drawer_view_conversation_header_content_type_public',
+        HEADER_SELF: 'core_message/message_drawer_view_conversation_header_content_type_self',
         DAY: 'core_message/message_drawer_view_conversation_body_day',
         MESSAGE: 'core_message/message_drawer_view_conversation_body_message',
         MESSAGES: 'core_message/message_drawer_view_conversation_body_messages'
     };
 
+    // Conversation types. They must have the same values defined in \core_message\api.
     var CONVERSATION_TYPES = {
         PRIVATE: 1,
-        PUBLIC: 2
+        PUBLIC: 2,
+        SELF: 3
+    };
+
+    // Categories displayed in the message drawer. Some methods (such as filterCountsByType) are expecting their value
+    // will be the same as the defined in the CONVERSATION_TYPES, except for the favourite.
+    var CONVERSATION_CATEGORY_TYPES = {
+        PRIVATE: 1,
+        PUBLIC: 2,
+        FAVOURITE: null
     };
 
     return {
         SELECTORS: SELECTORS,
         TEMPLATES: TEMPLATES,
         CONVERSATION_TYPES: CONVERSATION_TYPES,
+        CONVERSATION_CATEGORY_TYPES: CONVERSATION_CATEGORY_TYPES,
         NEWEST_MESSAGES_FIRST: true,
         LOAD_MESSAGE_LIMIT: 100,
         INITIAL_NEW_MESSAGE_POLL_TIMEOUT: 1000
index 43ea13c..a0a297c 100644 (file)
@@ -313,6 +313,37 @@ function(
         return null;
     };
 
+    /**
+     * Build a patch for the header of this conversation. Check if this conversation
+     * is a group conversation.
+     *
+     * @param  {Object} state The current state.
+     * @param  {Object} newState The new state.
+     * @return {Object} patch
+     */
+    var buildHeaderPatchTypeSelf = function(state, newState) {
+        var shouldRenderHeader = (state.name === null);
+
+        if (shouldRenderHeader) {
+            return {
+                type: Constants.CONVERSATION_TYPES.SELF,
+                // Don't display the controls for the self-conversations.
+                showControls: false,
+                context: {
+                    id: newState.id,
+                    name: newState.name,
+                    subname: newState.subname,
+                    imageurl: newState.imageUrl,
+                    isfavourite: newState.isFavourite,
+                    // Don't show favouriting if we don't have a conversation.
+                    showfavourite: newState.id !== null,
+                    showonlinestatus: true,
+                }
+            };
+        }
+
+        return null;
+    };
 
     /**
      * Build a patch for the header of this conversation. Check if this conversation
@@ -531,11 +562,11 @@ function(
      *
      * @param  {Object} state The current state.
      * @param  {Object} newState The new state.
-     * @return {Bool|Null}
+     * @return {int|Null} The conversation type of the messages to be deleted.
      */
     var buildConfirmDeleteSelectedMessages = function(state, newState) {
         if (newState.pendingDeleteMessageIds.length) {
-            return true;
+            return newState.type;
         } else if (state.pendingDeleteMessageIds.length) {
             return false;
         }
@@ -548,11 +579,11 @@ function(
      *
      * @param  {Object} state The current state.
      * @param  {Object} newState The new state.
-     * @return {Bool|Null}
+     * @return {int|Null} The conversation type to be deleted.
      */
     var buildConfirmDeleteConversation = function(state, newState) {
         if (!state.pendingDeleteConversation && newState.pendingDeleteConversation) {
-            return true;
+            return newState.type;
         } else if (state.pendingDeleteConversation && !newState.pendingDeleteConversation) {
             return false;
         }
@@ -948,6 +979,11 @@ function(
         var oldOtherUser = getOtherUserFromState(state);
         var newOtherUser = getOtherUserFromState(newState);
 
+        if (newState.type == Constants.CONVERSATION_TYPES.SELF) {
+            // Users always can send message themselves on self-conversations.
+            return null;
+        }
+
         if (!oldOtherUser && !newOtherUser) {
             return null;
         } else if (oldOtherUser && !newOtherUser) {
@@ -1104,6 +1140,19 @@ function(
         return null;
     };
 
+    /**
+     * We should show this message always, for all the self-conversations.
+     *
+     * The message should be hidden when it's not a self-conversation.
+     *
+     * @param  {Object} state The current state.
+     * @param  {Object} newState The new state.
+     * @return {bool}
+     */
+    var buildSelfConversationMessage = function(state, newState) {
+        return (newState.type == Constants.CONVERSATION_TYPES.SELF);
+    };
+
     /**
      * We should show the contact request sent message if the user just sent
      * a contact request to the other user and there are no messages in the
@@ -1190,6 +1239,13 @@ function(
             header: buildHeaderPatchTypePublic,
             footer: buildFooterPatchTypePublic,
         };
+        // These build functions are only applicable to self-conversations.
+        config[Constants.CONVERSATION_TYPES.SELF] = {
+            header: buildHeaderPatchTypeSelf,
+            footer: buildFooterPatchTypePublic,
+            confirmDeleteConversation: buildConfirmDeleteConversation,
+            selfConversationMessage: buildSelfConversationMessage
+        };
 
         var patchConfig = $.extend({}, config.all);
         if (newState.type && newState.type in config) {
index d0b67f2..77be40a 100644 (file)
@@ -75,6 +75,26 @@ function(
         getMessagesContainer(body).addClass('hidden');
     };
 
+    /**
+     * Get the self-conversation message container element.
+     *
+     * @param  {Object} body Conversation body container element.
+     * @return {Object} The messages container element.
+     */
+    var getSelfConversationMessageContainer = function(body) {
+        return body.find(SELECTORS.SELF_CONVERSATION_MESSAGE_CONTAINER);
+    };
+
+    /**
+     * Hide the self-conversation message container element.
+     *
+     * @param  {Object} body Conversation body container element.
+     * @return {Object} The messages container element.
+     */
+    var hideSelfConversationMessageContainer = function(body) {
+        return getSelfConversationMessageContainer(body).addClass('hidden');
+    };
+
     /**
      * Get the contact request sent container element.
      *
@@ -783,6 +803,8 @@ function(
 
         if (data.type == CONVERSATION_TYPES.PRIVATE) {
             template = data.showControls ? TEMPLATES.HEADER_PRIVATE : TEMPLATES.HEADER_PRIVATE_NO_CONTROLS;
+        } else if (data.type == CONVERSATION_TYPES.SELF) {
+            template = TEMPLATES.HEADER_SELF;
         }
 
         return Templates.render(template, data.context)
@@ -1121,12 +1143,21 @@ function(
      * @param {Object} header The header container element.
      * @param {Object} body The body container element.
      * @param {Object} footer The footer container element.
-     * @param {Bool} show If the dialogue should show.
+     * @param {int|Null} type The messages conversation type to be removed.
      * @return {Object} jQuery promise
      */
-    var renderConfirmDeleteSelectedMessages = function(header, body, footer, show) {
-        if (show) {
-            return Str.get_string('deleteselectedmessagesconfirm', 'core_message')
+    var renderConfirmDeleteSelectedMessages = function(header, body, footer, type) {
+        var showmessage = null;
+        if (type == CONVERSATION_TYPES.SELF) {
+            // Message displayed to self-conversations is slighly different.
+            showmessage = 'deleteselectedmessagesconfirmselfconversation';
+        } else if (type) {
+            // This other message should be displayed.
+            showmessage = 'deleteselectedmessagesconfirm';
+        }
+
+        if (showmessage) {
+            return Str.get_string(showmessage, 'core_message')
                 .then(function(string) {
                     return showConfirmDialogue(
                         header,
@@ -1150,12 +1181,21 @@ function(
      * @param {Object} header The header container element.
      * @param {Object} body The body container element.
      * @param {Object} footer The footer container element.
-     * @param {Bool} show If the dialogue should show
+     * @param {int|Null} type The conversation type to be removed.
      * @return {Object} jQuery promise
      */
-    var renderConfirmDeleteConversation = function(header, body, footer, show) {
-        if (show) {
-            return Str.get_string('deleteallconfirm', 'core_message')
+    var renderConfirmDeleteConversation = function(header, body, footer, type) {
+        var showmessage = null;
+        if (type == CONVERSATION_TYPES.SELF) {
+            // Message displayed to self-conversations is slighly different.
+            showmessage = 'deleteallselfconfirm';
+        } else if (type) {
+            // This other message should be displayed.
+            showmessage = 'deleteallconfirm';
+        }
+
+        if (showmessage) {
+            return Str.get_string(showmessage, 'core_message')
                 .then(function(string) {
                     return showConfirmDialogue(
                         header,
@@ -1437,6 +1477,25 @@ function(
         }
     };
 
+    /**
+     * Show or hide the self-conversation message.
+     *
+     * @param {Object} header The header container element.
+     * @param {Object} body The body container element.
+     * @param {Object} footer The footer container element.
+     * @param {Object} displayMessage should the message be displayed?.
+     * @return {Object|true} jQuery promise
+     */
+    var renderSelfConversationMessage = function(header, body, footer, displayMessage) {
+        var container = getSelfConversationMessageContainer(body);
+        if (displayMessage) {
+            container.removeClass('hidden');
+        } else {
+            container.addClass('hidden');
+        }
+        return true;
+    };
+
     /**
      * Show or hide the require add contact panel.
      *
@@ -1472,6 +1531,7 @@ function(
     var renderReset = function(header, body, footer) {
         hideConfirmDialogue(header, body, footer);
         hideContactRequestSentContainer(body);
+        hideSelfConversationMessageContainer(body);
         hideAllHeaderElements(header);
         showHeaderPlaceholder(header);
         hideAllFooterElements(footer);
@@ -1499,6 +1559,7 @@ function(
                 confirmDeleteConversation: renderConfirmDeleteConversation,
                 confirmContactRequest: renderConfirmContactRequest,
                 requireAddContact: renderRequireAddContact,
+                selfConversationMessage: renderSelfConversationMessage,
                 contactRequestSent: renderContactRequestSent
             },
             {
index 74f4d71..d126748 100644 (file)
@@ -30,7 +30,8 @@ define(
     'core_message/message_drawer_routes',
     'core_message/message_drawer_events',
     'core_message/message_drawer_view_overview_section',
-    'core_message/message_repository'
+    'core_message/message_repository',
+    'core_message/message_drawer_view_conversation_constants'
 ],
 function(
     $,
@@ -41,7 +42,8 @@ function(
     Routes,
     MessageDrawerEvents,
     Section,
-    MessageRepository
+    MessageRepository,
+    Constants
 ) {
 
     var SELECTORS = {
@@ -53,12 +55,6 @@ function(
         SECTION_TOGGLE_BUTTON: '[data-toggle]'
     };
 
-    var CONVERSATION_TYPES = {
-        PRIVATE: 1,
-        PUBLIC: 2,
-        FAVOURITE: null
-    };
-
     var loadAllCountsPromise = null;
 
     /**
@@ -89,7 +85,15 @@ function(
      * @return {Number}
      */
     var filterCountsByType = function(counts, type) {
-        return type === CONVERSATION_TYPES.FAVOURITE ? counts.favourites : counts.types[type];
+        var total = 0;
+        if (type === Constants.CONVERSATION_CATEGORY_TYPES.PRIVATE && counts.types[Constants.CONVERSATION_TYPES.SELF]) {
+            // As private and self conversations are displayed together, we need to add the counts for the self-conversations
+            // to the private ones, when there is any self-conversation.
+            total = counts.types[Constants.CONVERSATION_TYPES.PRIVATE] + counts.types[Constants.CONVERSATION_TYPES.SELF];
+        } else {
+            total = type === Constants.CONVERSATION_CATEGORY_TYPES.FAVOURITE ? counts.favourites : counts.types[type];
+        }
+        return total;
     };
 
     /**
@@ -226,11 +230,11 @@ function(
 
         var sections = [
             // Favourite conversations section.
-            [body.find(SELECTORS.FAVOURITES), CONVERSATION_TYPES.FAVOURITE, true],
+            [body.find(SELECTORS.FAVOURITES), Constants.CONVERSATION_CATEGORY_TYPES.FAVOURITE, true],
             // Group conversations section.
-            [body.find(SELECTORS.GROUP_MESSAGES), CONVERSATION_TYPES.PUBLIC, false],
+            [body.find(SELECTORS.GROUP_MESSAGES), Constants.CONVERSATION_CATEGORY_TYPES.PUBLIC, false],
             // Private conversations section.
-            [body.find(SELECTORS.MESSAGES), CONVERSATION_TYPES.PRIVATE, false]
+            [body.find(SELECTORS.MESSAGES), Constants.CONVERSATION_CATEGORY_TYPES.PRIVATE, false]
         ];
 
         sections.forEach(function(args) {
index 01dba3e..a0ab83c 100644 (file)
@@ -183,14 +183,21 @@ function(
                 lastmessage: lastMessage ? $(lastMessage.text).text() || lastMessage.text : null
             };
 
-            if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
-                var otherUser = conversation.members.reduce(function(carry, member) {
+            var otherUser = null;
+            if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF) {
+                // Self-conversations have only one member.
+                otherUser = conversation.members[0];
+            } else if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE) {
+                // For private conversations, remove the current userId from the members to get the other user.
+                otherUser = conversation.members.reduce(function(carry, member) {
                     if (!carry && member.id != userId) {
                         carry = member;
                     }
                     return carry;
                 }, null);
+            }
 
+            if (otherUser !== null) {
                 formattedConversation.userid = otherUser.id;
                 formattedConversation.showonlinestatus = otherUser.showonlinestatus;
                 formattedConversation.isonline = otherUser.isonline;
@@ -238,7 +245,8 @@ function(
                     type,
                     LOAD_LIMIT + 1,
                     offset,
-                    includeFavourites
+                    includeFavourites,
+                    true // Always merge self-conversations with private conversations, to display them together.
                 )
                 .then(function(response) {
                     var conversations = response.conversations;
@@ -575,8 +583,13 @@ function(
         });
 
         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, function(conversation) {
+            // Self-conversations could be displayed as private conversations when they are not starred. So we need to exclude
+            // them from the following check to make sure last messages are updated properly for them.
             if (
-                (type && conversation.type != type) ||
+                (type && conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF &&
+                type != MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE && !conversation.isFavourite) ||
+                (type && conversation.type != MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF &&
+                type != conversation.type) ||
                 (includeFavourites && !conversation.isFavourite) ||
                 (!includeFavourites && conversation.isFavourite)
             ) {
@@ -613,7 +626,11 @@ function(
                 if (!conversationElement.length) {
                     createNewConversation(root, conversation);
                 }
-            } else if (type == conversation.type) {
+            } else if (type == conversation.type ||
+                    (type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE &&
+                     conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF)) {
+                // Self-conversations are displayed in the private conversations section, so they should be removed from
+                // there when they are favourited.
                 conversationElement = getConversationElement(root, conversation.id);
                 if (conversationElement.length) {
                     deleteConversation(root, conversationElement);
@@ -628,7 +645,11 @@ function(
                 if (conversationElement.length) {
                     deleteConversation(root, conversationElement);
                 }
-            } else if (type == conversation.type) {
+            } else if (type == conversation.type ||
+                    (type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PRIVATE &&
+                     conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF)) {
+                // Self-conversations are displayed in the private conversations section, so they should be added
+                // there when they are unfavourited.
                 conversationElement = getConversationElement(root, conversation.id);
                 if (!conversationElement.length) {
                     createNewConversation(root, conversation);
index 24b2349..31c630d 100644 (file)
  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notification) {
-
-    var CONVERSATION_TYPES = {
-        PRIVATE: 1,
-        PUBLIC: 2
-    };
+define(
+[
+    'jquery',
+    'core/ajax',
+    'core/notification',
+    'core_message/message_drawer_view_conversation_constants'
+], function(
+    $,
+    Ajax,
+    Notification,
+    Constants) {
+
+    var CONVERSATION_TYPES = Constants.CONVERSATION_TYPES;
 
     /**
      * Retrieve a list of messages from the server.
@@ -771,6 +778,45 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
         return Ajax.call([request])[0];
     };
 
+    /**
+     * Get a self-conversation.
+     *
+     * @param {int} loggedInUserId The logged in user
+     * @param {int} messageLimit Limit for messages
+     * @param {int} messageOffset Offset for messages
+     * @param {bool} newestMessagesFirst Order the messages by newest first
+     * @return {object} jQuery promise
+     */
+    var getSelfConversation = function(
+        loggedInUserId,
+        messageLimit,
+        messageOffset,
+        newestMessagesFirst
+    ) {
+        var args = {
+            userid: loggedInUserId
+        };
+
+        if (typeof messageLimit != 'undefined' && messageLimit !== null) {
+            args.messagelimit = messageLimit;
+        }
+
+        if (typeof messageOffset != 'undefined' && messageOffset !== null) {
+            args.messageoffset = messageOffset;
+        }
+
+        if (typeof newestMessagesFirst != 'undefined' && newestMessagesFirst !== null) {
+            args.newestmessagesfirst = newestMessagesFirst;
+        }
+
+        var request = {
+            methodname: 'core_message_get_self_conversation',
+            args: args
+        };
+
+        return Ajax.call([request])[0];
+    };
+
     /**
      * Get the conversations for a user.
      *
@@ -786,7 +832,8 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
         type,
         limit,
         offset,
-        favourites
+        favourites,
+        mergeself
     ) {
         var args = {
             userid: userId,
@@ -805,6 +852,10 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
             args.favourites = favourites;
         }
 
+        if (typeof mergeself != 'undefined' && mergeself !== null) {
+            args.mergeself = mergeself;
+        }
+
         var request = {
             methodname: 'core_message_get_conversations',
             args: args
@@ -814,7 +865,7 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
             .then(function(result) {
                 if (result.conversations.length) {
                     result.conversations = result.conversations.map(function(conversation) {
-                        if (conversation.type == CONVERSATION_TYPES.PRIVATE) {
+                        if (conversation.type == CONVERSATION_TYPES.PRIVATE || conversation.type == CONVERSATION_TYPES.SELF) {
                             var otherUser = conversation.members.length ? conversation.members[0] : null;
 
                             if (otherUser) {
@@ -1093,6 +1144,7 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
         declineContactRequest: declineContactRequest,
         getConversation: getConversation,
         getConversationBetweenUsers: getConversationBetweenUsers,
+        getSelfConversation: getSelfConversation,
         getConversations: getConversations,
         getConversationMembers: getConversationMembers,
         setFavouriteConversations: setFavouriteConversations,
index 8a56fdc..b183404 100644 (file)
@@ -78,6 +78,11 @@ class api {
      */
     const MESSAGE_CONVERSATION_TYPE_GROUP = 2;
 
+    /**
+     * A self conversation.
+     */
+    const MESSAGE_CONVERSATION_TYPE_SELF = 3;
+
     /**
      * The state for an enabled conversation area.
      */
@@ -125,14 +130,15 @@ class api {
              LEFT JOIN {message_user_actions} mua
                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
                  WHERE (m.useridfrom = ? OR mcm.userid = ?)
-                   AND m.useridfrom != mcm.userid
+                   AND (m.useridfrom != mcm.userid OR mc.type = ?)
                    AND u.deleted = 0
                    AND u2.deleted = 0
                    AND mua.id is NULL
                    AND " . $DB->sql_like('smallmessage', '?', false) . "
               ORDER BY timecreated DESC";
 
-        $params = array($userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid, '%' . $search . '%');
+        $params = array($userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid,
+            self::MESSAGE_CONVERSATION_TYPE_SELF, '%' . $search . '%');
 
         // Convert the messages into searchable contacts with their last message being the message that was searched.
         $conversations = array();
@@ -316,7 +322,11 @@ class api {
         $fullname = $DB->sql_fullname();
 
         // Users not to include.
-        $excludeusers = array($userid, $CFG->siteguest);
+        $excludeusers = array($CFG->siteguest);
+        if (!$selfconversation = self::get_self_conversation($userid)) {
+            // Userid should only be excluded when she hasn't a self-conversation.
+            $excludeusers[] = $userid;
+        }
         list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
 
         $params = array('search' => '%' . $DB->sql_like_escape($search) . '%', 'userid1' => $userid, 'userid2' => $userid);
@@ -427,7 +437,12 @@ class api {
         if (!empty($foundusers)) {
             $noncontacts = helper::get_member_info($userid, array_keys($foundusers));
             foreach ($noncontacts as $memberuserid => $memberinfo) {
-                $noncontacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000);
+                if ($memberuserid !== $userid) {
+                    $noncontacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0,
+                        1000);
+                } else {
+                    $noncontacts[$memberuserid]->conversations[$selfconversation->id] = $selfconversation;
+                }
             }
         }
 
@@ -514,15 +529,17 @@ class api {
      * @param int $limitnum
      * @param int $type the type of the conversation, if you wish to filter to a certain type (see api constants).
      * @param bool $favourites whether to include NO favourites (false) or ONLY favourites (true), or null to ignore this setting.
+     * @param bool $mergeself whether to include self-conversations (true) or ONLY private conversations (false)
+     *             when private conversations are requested.
      * @return array the array of conversations
      * @throws \moodle_exception
      */
     public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20, int $type = null,
-            bool $favourites = null) {
+            bool $favourites = null, bool $mergeself = false) {
         global $DB;
 
         if (!is_null($type) && !in_array($type, [self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
-                self::MESSAGE_CONVERSATION_TYPE_GROUP])) {
+                self::MESSAGE_CONVERSATION_TYPE_GROUP, self::MESSAGE_CONVERSATION_TYPE_SELF])) {
             throw new \moodle_exception("Invalid value ($type) for type param, please see api constants.");
         }
 
@@ -553,7 +570,18 @@ class api {
         }
 
         // If we need to restrict type, generate the SQL snippet.
-        $typesql = !is_null($type) ? " AND mc.type = :convtype " : "";
+        $typesql = "";
+        $typeparams = [];
+        if (!is_null($type)) {
+            if ($mergeself && $type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+                // When $megerself is set to true, the self-conversations are returned also with the private conversations.
+                $typesql = " AND (mc.type = :convtype1 OR mc.type = :convtype2) ";
+                $typeparams = ['convtype1' => $type, 'convtype2' => self::MESSAGE_CONVERSATION_TYPE_SELF];
+            } else {
+                $typesql = " AND mc.type = :convtype ";
+                $typeparams = ['convtype' => $type];
+            }
+        }
 
         $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
                        m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagetrust, m.fullmessagehtml, m.timecreated,
@@ -587,16 +615,16 @@ class api {
                   AND mc.enabled = 1 $typesql $favouritesql
               ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";
 
-        $params = array_merge($favouriteparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
-            'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED,
-            'convtype' => $type]);
+        $params = array_merge($favouriteparams, $typeparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
+            'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED]);
         $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
 
         $conversations = [];
-        $selfconversations = []; // Used to track legacy conversations with one's self (both conv members the same user).
+        $selfconversations = []; // Used to track conversations with one's self.
         $members = [];
         $individualmembers = [];
         $groupmembers = [];
+        $selfmembers = [];
         foreach ($conversationset as $conversation) {
             $conversations[$conversation->id] = $conversation;
             $members[$conversation->id] = [];
@@ -621,13 +649,12 @@ class api {
         //
         // For 'individual' type conversations between 2 users, regardless of who sent the last message,
         // we want the details of the other member in the conversation (i.e. not the current user).
-        // The only exception to the 'not the current user' rule is for 'self' conversations - a legacy construct in which a user
-        // can message themselves via user bulk actions. Subsequently, there are 2 records for the same user created in the members
-        // table.
         //
         // For 'group' type conversations, we want the details of the member who sent the last message, if there is one.
         // This can be the current user or another group member, but for groups without messages, this will be empty.
         //
+        // For 'self' type conversations, we want the details of the current user.
+        //
         // This also means that if type filtering is specified and only group conversations are returned, we don't need this extra
         // query to get the 'other' user as we already have that information.
 
@@ -647,6 +674,10 @@ class api {
                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
                     $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
                 }
+            } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF) {
+                $selfconversations[$conversation->id] = $conversation->id;
+                $members[$conversation->id][$userid] = $userid;
+                $selfmembers[$userid] = $userid;
             }
         }
         // If we need to fetch any member information for any of the individual conversations.
@@ -665,23 +696,6 @@ class api {
                 $members[$member->conversationid][$member->userid] = $member->userid;
                 $individualmembers[$member->userid] = $member->userid;
             }
-
-            // Self conversations: If any of the individual conversations which were missing members are still missing members,
-            // we know these must be 'self' conversations. This is a legacy scenario, created via user bulk actions.
-            // In such cases, the member returned should be the current user.
-            //
-            // NOTE: Currently, these conversations are not returned by this method, however,
-            // identifying them is important for future reference.
-            foreach ($individualconversations as $indconvid) {
-                if (empty($members[$indconvid])) {
-                    // Keep track of the self conversation (for future use).
-                    $selfconversations[$indconvid] = $indconvid;
-
-                    // Set the member to the current user.
-                    $members[$indconvid][$userid] = $userid;
-                    $individualmembers[$userid] = $userid;
-                }
-            }
         }
 
         // We could fail early here if we're sure that:
@@ -692,7 +706,7 @@ class api {
         // needs to be done in a separate query to avoid doing a join on the messages tables and the user
         // tables because on large sites these tables are massive which results in extremely slow
         // performance (typically due to join buffer exhaustion).
-        if (!empty($individualmembers) || !empty($groupmembers)) {
+        if (!empty($individualmembers) || !empty($groupmembers) || !empty($selfmembers)) {
             // Now, we want to remove any duplicates from the group members array. For individual members we will
             // be doing a more extensive call as we want their contact requests as well as privacy information,
             // which is not necessary for group conversations.
@@ -700,9 +714,10 @@ class api {
 
             $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
             $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
+            $selfmemberinfo = helper::get_member_info($userid, $selfmembers);
 
             // Don't use array_merge, as we lose array keys.
-            $memberinfo = $individualmemberinfo + $groupmemberinfo;
+            $memberinfo = $individualmemberinfo + $groupmemberinfo + $selfmemberinfo;
 
             if (empty($memberinfo)) {
                 return [];
@@ -766,6 +781,17 @@ class api {
         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
             $userid, $userid]);
 
+        // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
+        $selfmessagessql = "SELECT COUNT(m.id)
+                              FROM {messages} m
+                        INNER JOIN {message_conversations} mc
+                                ON mc.id = m.conversationid
+                             WHERE mc.type = ? AND convhash = ?";
+        $selfmessagestotal = $DB->count_records_sql(
+            $selfmessagessql,
+            [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
+        );
+
         // Because we'll be calling format_string on each conversation name and passing contexts, we preload them here.
         // This warms the cache and saves potentially hitting the DB once for each context fetch below.
         \context_helper::preload_contexts_by_id(array_column($conversations, 'contextid'));
@@ -773,16 +799,14 @@ class api {
         // Now, create the final return structure.
         $arrconversations = [];
         foreach ($conversations as $conversation) {
-            // Do not include any individual conversations which do not contain a recent message for the user.
+            // Do not include any individual which do not contain a recent message for the user.
             // This happens if the user has deleted all messages.
+            // Exclude the self-conversations with messages but without a recent message because the user has deleted all them.
+            // Self-conversations without any message should be included, to display them first time they are created.
             // Group conversations with deleted users or no messages are always returned.
-            if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL
-                    && (empty($conversation->messageid))) {
-                continue;
-            }
-
-            // Exclude 'self' conversations for now.
-            if (isset($selfconversations[$conversation->id])) {
+            if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && empty($conversation->messageid) ||
+                   ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($conversation->messageid)
+                    && $selfmessagestotal > 0)) {
                 continue;
             }
 
@@ -923,10 +947,12 @@ class api {
             $memberoffset,
             $memberlimit
         );
-        // Strip out the requesting user to match what get_conversations does.
-        $members = array_filter($members, function($member) use ($userid) {
-            return $member->id != $userid;
-        });
+        if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_SELF) {
+            // Strip out the requesting user to match what get_conversations does, except for self-conversations.
+            $members = array_filter($members, function($member) use ($userid) {
+                return $member->id != $userid;
+            });
+        }
 
         $messages = self::get_conversation_messages(
             $userid,
@@ -1570,7 +1596,8 @@ class api {
         // Some restrictions we need to be aware of:
         // - Individual conversations containing soft-deleted user must be counted.
         // - Individual conversations containing only deleted messages must NOT be counted.
-        // - Individual conversations which are legacy 'self' conversations (2 members, both the same user) must NOT be counted.
+        // - Self-conversations with 0 messages must be counted.
+        // - Self-conversations containing only deleted messages must NOT be counted.
         // - Group conversations with 0 messages must be counted.
         // - Linked conversations which are disabled (enabled = 0) must NOT be counted.
         // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
@@ -1582,21 +1609,10 @@ class api {
         $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
         list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');
 
-        $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count
+        $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count, MAX(maxvisibleconvmessage.convid) as maxconvidmessage
                   FROM {message_conversations} mc
             INNER JOIN {message_conversation_members} mcm
                     ON mcm.conversationid = mc.id
-            INNER JOIN (
-                              SELECT mcm.conversationid, count(distinct mcm.userid) as membercount
-                                FROM {message_conversation_members} mcm
-                               WHERE mcm.conversationid IN (
-                                        SELECT DISTINCT conversationid
-                                          FROM {message_conversation_members} mcm2
-                                         WHERE userid = :userid5
-                                     )
-                            GROUP BY mcm.conversationid
-                       ) uniquemembercount
-                    ON uniquemembercount.conversationid = mc.id
              LEFT JOIN (
                               SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
                                 FROM {messages} m
@@ -1613,8 +1629,9 @@ class api {
                  WHERE mcm.userid = :userid3
                    AND mc.enabled = :enabled
                    AND (
-                          (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL AND membercount > 1) OR
-                          (mc.type = :grouptype)
+                          (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR
+                          (mc.type = :grouptype) OR
+                          (mc.type = :selftype)
                        )
               GROUP BY mc.type, fav.itemtype
               ORDER BY mc.type ASC";
@@ -1629,6 +1646,7 @@ class api {
             'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
             'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
             'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
+            'selftype' => self::MESSAGE_CONVERSATION_TYPE_SELF,
         ] + $favparams;
 
         // Assemble the return array.
@@ -1636,12 +1654,28 @@ class api {
             'favourites' => 0,
             'types' => [
                 self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                self::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                self::MESSAGE_CONVERSATION_TYPE_SELF => 0
             ]
         ];
 
+        // For the self-conversations, get the total number of messages (to know if the conversation is new or it has been emptied).
+        $selfmessagessql = "SELECT COUNT(m.id)
+                              FROM {messages} m
+                        INNER JOIN {message_conversations} mc
+                                ON mc.id = m.conversationid
+                             WHERE mc.type = ? AND convhash = ?";
+        $selfmessagestotal = $DB->count_records_sql(
+            $selfmessagessql,
+            [self::MESSAGE_CONVERSATION_TYPE_SELF, helper::get_conversation_hash([$userid])]
+        );
+
         $countsrs = $DB->get_recordset_sql($sql, $params);
         foreach ($countsrs as $key => $val) {
+            // Empty self-conversations with deleted messages should be excluded.
+            if ($val->type == self::MESSAGE_CONVERSATION_TYPE_SELF && empty($val->maxconvidmessage) && $selfmessagestotal > 0) {
+                continue;
+            }
             if (!empty($val->itemtype)) {
                 $counts['favourites'] += $val->count;
                 continue;
@@ -1862,7 +1896,8 @@ class api {
         // User can post messages and is in the conversation, but we need to check the conversation type to
         // know whether or not to check the user privacy settings via can_contact_user().
         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
-        if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
+        if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP ||
+            $conversation->type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
             return true;
         } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
             // Get the other user in the conversation.
@@ -2382,6 +2417,23 @@ class api {
         return $conversations;
     }
 
+    /**
+     * Returns the self conversation for a user.
+     *
+     * @param int $userid The user id to get the self-conversations
+     * @return \stdClass|false The self-conversation object or false if it doesn't exist
+     * @since Moodle 3.7
+     */
+    public static function get_self_conversation(int $userid) {
+        global $DB;
+
+        $conditions = [
+            'type' => self::MESSAGE_CONVERSATION_TYPE_SELF,
+            'convhash' => helper::get_conversation_hash([$userid])
+        ];
+        return $DB->get_record('message_conversations', $conditions);
+    }
+
     /**
      * Creates a conversation between two users.
      *
@@ -2420,7 +2472,8 @@ class api {
 
         $validtypes = [
             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
-            self::MESSAGE_CONVERSATION_TYPE_GROUP
+            self::MESSAGE_CONVERSATION_TYPE_GROUP,
+            self::MESSAGE_CONVERSATION_TYPE_SELF
         ];
 
         if (!in_array($type, $validtypes)) {
@@ -2432,13 +2485,20 @@ class api {
             if (count($userids) > 2) {
                 throw new \moodle_exception('An individual conversation can not have more than two users.');
             }
+            if ($userids[0] == $userids[1]) {
+                throw new \moodle_exception('Trying to create an individual conversation instead of a self conversation.');
+            }
+        } else if ($type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
+            if (count($userids) != 1) {
+                throw new \moodle_exception('A self conversation can not have more than one user.');
+            }
         }
 
         $conversation = new \stdClass();
         $conversation->type = $type;
         $conversation->name = $name;
         $conversation->convhash = null;
-        if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+        if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL || $type == self::MESSAGE_CONVERSATION_TYPE_SELF) {
             $conversation->convhash = helper::get_conversation_hash($userids);
         }
         $conversation->component = $component;
@@ -2840,8 +2900,9 @@ class api {
      * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
      */
     protected static function can_contact_user(int $recipientid, int $senderid) : bool {
-        if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid)) {
-            // The sender has the ability to contact any user across the entire site.
+        if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid) ||
+            $recipientid == $senderid) {
+            // The sender has the ability to contact any user across the entire site or themselves.
             return true;
         }
 
@@ -3146,7 +3207,8 @@ class api {
         // Assemble the return array.
         $counts = ['favourites' => 0, 'types' => [
             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-            self::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+            self::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+            self::MESSAGE_CONVERSATION_TYPE_SELF => 0
         ]];
         foreach ($unreadcounts as $convid => $info) {
             if (isset($favouriteconvids[$convid])) {
index ee90f5c..2bba6da 100644 (file)
@@ -949,14 +949,19 @@ class provider implements
             // Get subcontext.
             if (empty($conversation->contextid)) {
                 // Conversations without context are stored in 'Messages | <Other user id>'.
-                $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation->id]);
-                $members = array_filter($members, function ($member) use ($userid) {
-                    return $member->userid != $userid;
-                });
-                if ($otheruser = reset($members)) {
-                    $otherusertext = $otheruser->userid;
+                if ($conversation->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF) {
+                    // This is a self-conversation. The other user is the same userid.
+                    $otherusertext = $userid;
                 } else {
-                    $otherusertext = get_string('unknownuser', 'core_message') . '_' . $conversation->id;
+                    $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation->id]);
+                    $members = array_filter($members, function ($member) use ($userid) {
+                        return $member->userid != $userid;
+                    });
+                    if ($otheruser = reset($members)) {
+                        $otherusertext = $otheruser->userid;
+                    } else {
+                        $otherusertext = get_string('unknownuser', 'core_message') . '_' . $conversation->id;
+                    }
                 }
 
                 $subcontext = array_merge(
index 2cf5dbd..cafc108 100644 (file)
@@ -122,7 +122,18 @@ class migrate_message_data extends \core\task\adhoc_task {
     private function migrate_data($userid, $otheruserid) {
         global $DB;
 
-        if (!$conversationid = \core_message\api::get_conversation_between_users([$userid, $otheruserid])) {
+        if ($userid == $otheruserid) {
+            // Since 3.7, pending self-conversations should be migrated during the upgrading process so shouldn't be any
+            // self-conversations on the legacy tables. However, this extra-check has been added just in case.
+            $conversation = \core_message\api::get_self_conversation($userid);
+            if (empty($conversation)) {
+                $conversation = \core_message\api::create_conversation(
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+                    [$userid]
+                );
+            }
+            $conversationid = $conversation->id;
+        } else if (!$conversationid = \core_message\api::get_conversation_between_users([$userid, $otheruserid])) {
             $conversation = \core_message\api::create_conversation(
                 \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
                 [
index 7535be9..f7edd53 100644 (file)
@@ -1247,7 +1247,7 @@ class core_message_external extends external_api {
                 'name' => new external_value(PARAM_TEXT, 'The conversation name, if set', VALUE_DEFAULT, null),
                 'subname' => new external_value(PARAM_TEXT, 'A subtitle for the conversation name, if set', VALUE_DEFAULT, null),
                 'imageurl' => new external_value(PARAM_URL, 'A link to the conversation picture, if set', VALUE_DEFAULT, null),
-                'type' => new external_value(PARAM_INT, 'The type of the conversation (1=individual,2=group)'),
+                'type' => new external_value(PARAM_INT, 'The type of the conversation (1=individual,2=group,3=self)'),
                 'membercount' => new external_value(PARAM_INT, 'Total number of conversation members'),
                 'ismuted' => new external_value(PARAM_BOOL, 'If the user muted this conversation'),
                 'isfavourite' => new external_value(PARAM_BOOL, 'If the user marked this conversation as a favourite'),
@@ -1752,7 +1752,9 @@ class core_message_external extends external_api {
                 'favourites' => new external_value(PARAM_BOOL, 'Whether to restrict the results to contain NO favourite
                 conversations (false), ONLY favourite conversation (true), or ignore any restriction altogether (null)',
                     VALUE_DEFAULT, null),
-
+                'mergeself' => new external_value(PARAM_BOOL, 'Whether to include self-conversations (true) or ONLY private
+                    conversations (false) when private conversations are requested.',
+                    VALUE_DEFAULT, false),
             )
         );
     }
@@ -1765,11 +1767,14 @@ class core_message_external extends external_api {
      * @param int $limitnum
      * @param int|null $type
      * @param bool|null $favourites
+     * @param bool $mergeself whether to include self-conversations (true) or ONLY private conversations (false)
+     *             when private conversations are requested.
      * @return stdClass
      * @throws \moodle_exception if the messaging feature is disabled on the site.
      * @since 3.2
      */
-    public static function get_conversations($userid, $limitfrom = 0, $limitnum = 0, int $type = null, bool $favourites = null) {
+    public static function get_conversations($userid, $limitfrom = 0, $limitnum = 0, int $type = null, bool $favourites = null,
+            bool $mergeself = false) {
         global $CFG, $USER;
 
         // All the standard BL checks.
@@ -1782,7 +1787,8 @@ class core_message_external extends external_api {
             'limitfrom' => $limitfrom,
             'limitnum' => $limitnum,
             'type' => $type,
-            'favourites' => $favourites
+            'favourites' => $favourites,
+            'mergeself' => $mergeself
         );
         $params = self::validate_parameters(self::get_conversations_parameters(), $params);
 
@@ -1798,7 +1804,8 @@ class core_message_external extends external_api {
             $params['limitfrom'],
             $params['limitnum'],
             $params['type'],
-            $params['favourites']
+            $params['favourites'],
+            $params['mergeself']
         );
 
         return (object) ['conversations' => $conversations];
@@ -2025,6 +2032,91 @@ class core_message_external extends external_api {
         return self::get_conversation_structure(true);
     }
 
+    /**
+     * Get self-conversation parameters.
+     *
+     * @return external_function_parameters
+     */
+    public static function get_self_conversation_parameters() {
+        return new external_function_parameters(
+            array(
+                'userid' => new external_value(PARAM_INT, 'The id of the user who we are viewing self-conversations for'),
+                'messagelimit' => new external_value(PARAM_INT, 'Limit for number of messages', VALUE_DEFAULT, 100),
+                'messageoffset' => new external_value(PARAM_INT, 'Offset for messages list', VALUE_DEFAULT, 0),
+                'newestmessagesfirst' => new external_value(PARAM_BOOL, 'Order messages by newest first', VALUE_DEFAULT, true)
+            )
+        );
+    }
+
+    /**
+     * Get a single self-conversation.
+     *
+     * @param int $userid The user id to get the self-conversation for
+     * @param int $messagelimit Limit number of messages to load
+     * @param int $messageoffset Offset the messages
+     * @param bool $newestmessagesfirst Order messages by newest first
+     * @return stdClass
+     * @throws \moodle_exception if the messaging feature is disabled on the site.
+     * @since Moodle 3.7
+     */
+    public static function get_self_conversation(
+        int $userid,
+        int $messagelimit = 0,
+        int $messageoffset = 0,
+        bool $newestmessagesfirst = true
+    ) {
+        global $CFG;
+
+        // All the standard BL checks.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        $params = [
+            'userid' => $userid,
+            'messagelimit' => $messagelimit,
+            'messageoffset' => $messageoffset,
+            'newestmessagesfirst' => $newestmessagesfirst
+        ];
+        self::validate_parameters(self::get_self_conversation_parameters(), $params);
+
+        $systemcontext = context_system::instance();
+        self::validate_context($systemcontext);
+
+        $conversation = \core_message\api::get_self_conversation($params['userid']);
+
+        if ($conversation) {
+            $conversation = \core_message\api::get_conversation(
+                $params['userid'],
+                $conversation->id,
+                false,
+                false,
+                0,
+                0,
+                $params['messagelimit'],
+                $params['messageoffset'],
+                $params['newestmessagesfirst']
+            );
+        }
+
+        if ($conversation) {
+            return $conversation;
+        } else {
+            // We have to throw an exception here because the external functions annoyingly
+            // don't accept null to be returned for a single structure.
+            throw new \moodle_exception('errorconversationdoesnotexist', 'message');
+        }
+    }
+
+    /**
+     * Get conversation returns.
+     *
+     * @return external_single_structure
+     */
+    public static function get_self_conversation_returns() {
+        return self::get_conversation_structure();
+    }
+
     /**
      * The messagearea conversations parameters.
      *
@@ -4625,6 +4717,8 @@ class core_message_external extends external_api {
                             'Total number of individual conversations'),
                         \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => new external_value(PARAM_INT,
                             'Total number of group conversations'),
+                        \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => new external_value(PARAM_INT,
+                            'Total number of self conversations'),
                     ]
                 ),
             ]
@@ -4700,6 +4794,8 @@ class core_message_external extends external_api {
                             'Total number of unread individual conversations'),
                         \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => new external_value(PARAM_INT,
                             'Total number of unread group conversations'),
+                        \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => new external_value(PARAM_INT,
+                            'Total number of unread self conversations'),
                     ]
                 ),
             ]
index a87e915..c584e01 100644 (file)
     style="overflow-y: auto; overflow-x: hidden"
 >
     <div class="position-relative h-100" data-region="content-container" style="overflow-y: auto; overflow-x: hidden">
+        <div class="p-3 text-center hidden" data-region="self-conversation-message-container">
+            <p class="m-0">{{#str}} selfconversation, core_message {{/str}}</p>
+            <p class="font-italic font-weight-light" data-region="text">{{#str}} selfconversationdefaultmessage, core_message {{/str}}</p>
+        </div>
         <div class="p-3 text-center hidden" data-region="contact-request-sent-message-container">
             <p class="m-0">{{#str}} contactrequestsent, core_message {{/str}}</p>
             <p class="font-italic font-weight-light" data-region="text"></p>
diff --git a/message/templates/message_drawer_view_conversation_header_content_type_self.mustache b/message/templates/message_drawer_view_conversation_header_content_type_self.mustache
new file mode 100644 (file)
index 0000000..9d4c1c1
--- /dev/null
@@ -0,0 +1,91 @@
+{{!
+    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/>.
+}}
+{{!
+    @template core_message/message_drawer_view_conversation_header_content_type_self
+
+    This template will render the header content of the conversation page in
+    the message message drawer.
+
+    Classes required for JS:
+    * none
+
+    Data attributes required for JS:
+    * All data attributes are required
+
+    Context variables required for this template:
+    * urls The URLs for the popover
+    * name
+    * subname
+    * imageurl
+    * isfavourite
+    * showonlinestatus
+
+
+    Example context (json):
+    {}
+
+}}
+
+<div class="d-flex align-items-center">
+    <div class="align-self-stretch" >
+        <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back>
+            {{> core_message/message_drawer_icon_back }}
+        </a>
+    </div>
+    <div class="d-flex text-truncate">
+        {{#imageurl}}
+            <div class="d-flex align-items-center">
+                <img
+                    class="rounded-circle"
+                    src="{{{.}}}"
+                    alt="{{name}}"
+                    aria-hidden="true"
+                    style="height: 38px"
+                >
+            </div>
+        {{/imageurl}}
+        <div class="w-100 text-truncate ml-2">
+            <div class="d-flex">
+                <strong class="m-0 text-truncate">{{name}}</strong>
+                <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
+                    {{#pix}} i/star-rating, core {{/pix}}
+                </span>
+            </div>
+            {{#showonlinestatus}}
+                <p class="m-0 font-weight-light text-truncate">
+                    {{#str}} loggedin, core_message {{/str}}
+                </p>
+            {{/showonlinestatus}}
+        </div>
+    </div>
+    <div class="ml-auto dropdown">
+        <button class="btn btn-link btn-icon icon-size-3" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+            {{#pix}} i/moremenu, core {{/pix}}
+        </button>
+        <div class="dropdown-menu pull-right">
+            <a class="dropdown-item {{#isfavourite}}hidden{{/isfavourite}} {{^showfavourite}}hidden{{/showfavourite}}" href="#" data-action="confirm-favourite">
+                {{#str}} addtofavourites, core_message {{/str}}
+            </a>
+            <a class="dropdown-item {{^isfavourite}}hidden{{/isfavourite}} {{^showfavourite}}hidden{{/showfavourite}}" href="#" data-action="confirm-unfavourite">
+                {{#str}} removefromfavourites, core_message {{/str}}
+            </a>
+            <a class="dropdown-item" href="#" data-action="request-delete-conversation">
+                {{#str}} deleteconversation, core_message {{/str}}
+            </a>
+        </div>
+    </div>
+</div>
\ No newline at end of file
index 81327e4..f86a2ff 100644 (file)
@@ -476,6 +476,35 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, $noncontacts[3]->conversations[$gc1->id]->type);
     }
 
+    /**
+     * Verify searching for users find themselves when they have self-conversations.
+     */
+    public function test_message_search_users_self_conversations() {
+        $this->resetAfterTest();
+
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User';
+        $user1->lastname = 'One';
+        $user1 = $this->getDataGenerator()->create_user($user1);
+        $user2 = new stdClass();
+        $user2->firstname = 'User';
+        $user2->lastname = 'Two';
+        $user2 = $this->getDataGenerator()->create_user($user2);
+
+        // Create self-conversation for user1.
+        $sc1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, [$user1->id]);
+        testhelper::send_fake_message_to_conversation($user1, $sc1->id, 'Hi myself!');
+
+        // Perform a search as user1.
+        $this->setUser($user1);
+        $result = \core_message\api::message_search_users($user1->id, 'One');
+
+        // Check user1 is found as non-contacts.
+        $this->assertCount(0, $result[0]);
+        $this->assertCount(1, $result[1]);
+    }
+
     /**
      * Verify searching for users works even if no matching users from either contacts, or non-contacts can be found.
      */
@@ -712,6 +741,62 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(0, \core_message\api::get_conversations_between_users($user6->id, $user1->id));
     }
 
+    /**
+     * Tests getting self-conversations.
+     */
+    public function test_get_self_conversation() {
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User';
+        $user1->lastname = 'One';
+        $user1 = self::getDataGenerator()->create_user($user1);
+
+        $user2 = new stdClass();
+        $user2->firstname = 'User';
+        $user2->lastname = 'Two';
+        $user2 = self::getDataGenerator()->create_user($user2);
+
+        $user3 = new stdClass();
+        $user3->firstname = 'User search';
+        $user3->lastname = 'Three';
+        $user3 = self::getDataGenerator()->create_user($user3);
+
+        // Add some users as contacts.
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user3->id, $user1->id);
+
+        // Create private conversations with some users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user1->id, $user2->id));
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user3->id, $user1->id));
+
+        // Create a group conversation with users.
+        $gc = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            array($user1->id, $user2->id, $user3->id),
+            'Project chat');
+
+        // Create self-conversations.
+        $sc1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+            array($user1->id));
+        $sc2 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+            array($user2->id));
+
+        // Send message to self-conversation.
+        testhelper::send_fake_message_to_conversation($user1, $sc1->id, 'Message to myself!');
+
+        $rsc1 = \core_message\api::get_self_conversation($user1->id);
+        $rsc2 = \core_message\api::get_self_conversation($user2->id);
+        $rsc3 = \core_message\api::get_self_conversation($user3->id);
+
+        // Check that we retrieved the correct conversations.
+        $this->assertEquals($sc1->id, $rsc1->id);
+        $this->assertEquals(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, $sc1->type);
+        $this->assertEquals($sc2->id, $rsc2->id);
+        $this->assertEquals(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, $sc2->type);
+        $this->assertFalse($rsc3);
+    }
+
     /**
      * Tests searching messages.
      */
@@ -726,6 +811,9 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         // The person doing the search.
         $this->setUser($user1);
 
+        // Create self-conversation.
+        $sc = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, [$user1->id]);
+
         // Create group conversation.
         $gc = \core_message\api::create_conversation(
             \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
@@ -734,12 +822,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Send some messages back and forth.
         $time = 1;
-        testhelper::send_fake_message_to_conversation($user1, $gc->id, 'My hero!', $time);
-        $this->send_fake_message($user3, $user1, 'Don\'t block me.', 0, $time + 1);
-        $this->send_fake_message($user1, $user2, 'Yo!', 0, $time + 2);
-        $this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 3);
-        $this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 4);
-        $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 5);
+        testhelper::send_fake_message_to_conversation($user1, $sc->id, 'Test message to self!', $time);
+        testhelper::send_fake_message_to_conversation($user1, $gc->id, 'My hero!', $time + 1);
+        $this->send_fake_message($user3, $user1, 'Don\'t block me.', 0, $time + 2);
+        $this->send_fake_message($user1, $user2, 'Yo!', 0, $time + 3);
+        $this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 4);
+        $this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 5);
+        $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 6);
 
         $convid = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]);
         $conv2id = \core_message\api::get_conversation_between_users([$user1->id, $user3->id]);
@@ -751,12 +840,12 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $messages = \core_message\api::search_messages($user1->id, 'o');
 
         // Confirm the data is correct.
-        $this->assertEquals(4, count($messages));
-
+        $this->assertEquals(5, count($messages));
         $message1 = $messages[0];
         $message2 = $messages[1];
         $message3 = $messages[2];
         $message4 = $messages[3];
+        $message5 = $messages[4];
 
         $this->assertEquals($user2->id, $message1->userid);
         $this->assertEquals($user2->id, $message1->useridfrom);
@@ -805,6 +894,18 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertTrue($message4->isread);
         $this->assertNull($message4->unreadcount);
         $this->assertEquals($gc->id, $message4->conversationid);
+
+        $this->assertEquals($user1->id, $message5->userid);
+        $this->assertEquals($user1->id, $message5->useridfrom);
+        $this->assertEquals(fullname($user1), $message5->fullname);
+        $this->assertTrue($message5->ismessaging);
+        $this->assertEquals('Test message to self!', $message5->lastmessage);
+        $this->assertNotEmpty($message5->messageid);
+        $this->assertFalse($message5->isonline);
+        $this->assertTrue($message5->isread);
+        $this->assertFalse($message5->isblocked);
+        $this->assertNull($message5->unreadcount);
+        $this->assertEquals($sc->id, $message5->conversationid);
     }
 
     /**
@@ -1431,28 +1532,64 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
     }
 
     /**
-     * Tests retrieving conversations when a legacy 'self' conversation exists.
+     * Tests retrieving conversations when a 'self' conversation exists.
      */
-    public function test_get_conversations_legacy_self_conversations() {
+    public function test_get_conversations_self_conversations() {
         global $DB;
 
-        // Create a legacy conversation between one user and themself.
+        // Create a conversation between one user and themself.
         $user1 = self::getDataGenerator()->create_user();
-        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
-            [$user1->id, $user1->id]);
-        testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Test message to self!');
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+        $user4 = self::getDataGenerator()->create_user();
+
+        // Create some individual conversations.
+        $ic1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+        $ic2 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user3->id]);
+        testhelper::send_fake_message_to_conversation($user1, $ic1->id, 'Message from user1 to user2');
+
+        // Create some self-conversations.
+        $sc1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, [$user1->id]);
+        $sc4 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, [$user4->id]);
+        testhelper::send_fake_message_to_conversation($user1, $sc1->id, 'Test message to self 1!');
 
         // Verify we are in a 'self' conversation state.
-        $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation->id]);
-        $this->assertCount(2, $members);
-        $member = array_pop($members);
-        $this->assertEquals($user1->id, $member->userid);
+        $members = $DB->get_records('message_conversation_members', ['conversationid' => $sc1->id]);
+        $this->assertCount(1, $members);
         $member = array_pop($members);
         $this->assertEquals($user1->id, $member->userid);
 
-        // Verify this conversation is not returned by the method.
-        $conversations = \core_message\api::get_conversations($user1->id);
-        $this->assertCount(0, $conversations);
+        // Verify the self-conversations are returned by the method.
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20, \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF);
+        $this->assertCount(1, $conversations);
+        $conversation = array_pop($conversations);
+        $this->assertEquals($conversation->id, $sc1->id);
+
+        $conversations = \core_message\api::get_conversations($user4->id);
+        // The self-conversation.
+        $this->assertCount(1, $conversations);
+
+        // Get only private conversations for user1 (empty conversations, like $ic2, are not returned).
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20,
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
+        $this->assertCount(1, $conversations);
+
+        // Merge self with private conversations for user1.
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20,
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, null, true);
+        $this->assertCount(2, $conversations);
+
+        // Get only private conversations for user2.
+        $conversations = \core_message\api::get_conversations($user2->id, 0, 20,
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
+        $this->assertCount(1, $conversations);
+
+        // Merge self with private conversations for user2 (is the same result than before because user2 hasn't self-conversations).
+        $conversations = \core_message\api::get_conversations($user2->id, 0, 20,
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, null, true);
+        $this->assertCount(1, $conversations);
     }
 
     /**
@@ -3033,15 +3170,22 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         // The person doing the search.
         $this->setUser($user1);
 
+        // Create self-conversation.
+        $sc1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, [$user1->id]);
+        $sc2 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, [$user2->id]);
+
         // Send some messages back and forth.
         $time = 1;
         $m1id = $this->send_fake_message($user1, $user2, 'Yo!', 0, $time + 1);
         $m2id = $this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 2);
         $m3id = $this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 3);
         $m4id = $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 4);
+        $m5id = testhelper::send_fake_message_to_conversation($user1, $sc1->id, 'Hi to myself!', $time + 5);
+        $m6id = testhelper::send_fake_message_to_conversation($user2, $sc2->id, 'I am talking with myself', $time + 6);
 
-        // Delete the conversation as user 1.
         $conversationid = \core_message\api::get_conversation_between_users([$user1->id, $user2->id]);
+
+        // Delete the individual conversation between user1 and user2 (only for user1).
         \core_message\api::delete_conversation_by_id($user1->id, $conversationid);
 
         $muas = $DB->get_records('message_user_actions', array(), 'timecreated ASC');
@@ -3069,6 +3213,26 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($user1->id, $mua4->userid);
         $this->assertEquals($m4id, $mua4->messageid);
         $this->assertEquals(\core_message\api::MESSAGE_ACTION_DELETED, $mua4->action);
+
+        // Delete the self-conversation as user 1.
+        \core_message\api::delete_conversation_by_id($user1->id, $sc1->id);
+
+        $muas = $DB->get_records('message_user_actions', array(), 'timecreated ASC');
+        $this->assertCount(5, $muas);
+
+        // Sort by id.
+        ksort($muas);
+
+        $mua1 = array_shift($muas);
+        $mua2 = array_shift($muas);
+        $mua3 = array_shift($muas);
+        $mua4 = array_shift($muas);
+        $mua5 = array_shift($muas);
+
+        // Check only messages in self-conversion for user1 are deleted (self-conversation for user2 shouldn't be removed).
+        $this->assertEquals($user1->id, $mua5->userid);
+        $this->assertEquals($m5id, $mua5->messageid);
+        $this->assertEquals(\core_message\api::MESSAGE_ACTION_DELETED, $mua5->action);
     }
 
     /**
@@ -3386,9 +3550,18 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             ]
         );
 
+        // Create a self-conversation for user1.
+        $sc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+            [$user1->id]
+        );
+
         // For group conversations, there are no user privacy checks, so only membership in the conversation is needed.
         $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
 
+        // For self conversations, there are no user privacy checks, so only membership in the conversation is needed.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $sc1->id));
+
         // For individual conversations, the default privacy setting of 'only contacts and course members' applies.
         // Users are not in the same course, nor are they contacts, so messages cannot be sent.
         $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
@@ -3538,9 +3711,16 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             ]
         );
 
+        // Create a self-conversation for user1.
+        $sc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+            [$user1->id]
+        );
+
         // Verify, non members cannot send a message.
         $this->assertFalse(\core_message\api::can_send_message_to_conversation($user4->id, $gc1->id));
         $this->assertFalse(\core_message\api::can_send_message_to_conversation($user4->id, $ic1->id));
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user4->id, $sc1->id));
     }
 
     /**
@@ -5776,6 +5956,34 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($user3->id, $request1->requesteduserid);
     }
 
+    /**
+     * Test returning members of a self conversation.
+     */
+    public function test_get_conversation_members_with_self_conversation() {
+        $lastaccess = new stdClass();
+        $lastaccess->lastaccess = time();
+
+        $user1 = self::getDataGenerator()->create_user($lastaccess);
+
+        $selfconversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+            [$user1->id]);
+        testhelper::send_fake_message_to_conversation($user1, $selfconversation->id, 'This is a self-message!');
+
+        // Get the members for the self-conversation.
+        $members = \core_message\api::get_conversation_members($user1->id, $selfconversation->id);
+        $this->assertCount(1, $members);
+
+        $member1 = array_shift($members);
+
+        // Confirm the standard fields are OK.
+        $this->assertEquals($user1->id, $member1->id);
+        $this->assertEquals(fullname($user1), $member1->fullname);
+        $this->assertEquals(true, $member1->isonline);
+        $this->assertEquals(true, $member1->showonlinestatus);
+        $this->assertEquals(false, $member1->iscontact);
+        $this->assertEquals(false, $member1->isblocked);
+    }
+
     /**
      * Test verifying that messages can be sent to existing individual conversations.
      */
@@ -5948,6 +6156,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
     public function test_get_conversation_counts_test_cases() {
         $typeindividual = \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL;
         $typegroup = \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP;
+        $typeself = \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF;
         list($user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8) = [0, 1, 2, 3, 4, 5, 6, 7];
         $conversations = [
             [
@@ -5986,11 +6195,11 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'enabled' => false
             ],
             [
-                'type' => $typeindividual,
-                'users' => [$user8, $user8],
-                'messages' => [$user8, $user8],
+                'type' => $typeself,
+                'users' => [$user8],
+                'messages' => [$user8],
                 'favourites' => [],
-                'enabled' => null // Individual conversations cannot be disabled.
+                'enabled' => null // Self-conversations cannot be disabled.
             ],
         ];
 
@@ -6002,11 +6211,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user5],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6017,11 +6228,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user4],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6032,11 +6245,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6047,11 +6262,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user2],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6062,11 +6279,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user4],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6077,11 +6296,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6092,11 +6313,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6107,11 +6330,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6122,11 +6347,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6137,11 +6364,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user2],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6152,11 +6381,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user3],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6167,11 +6398,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6182,11 +6415,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6197,11 +6432,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => [$user2]
             ],
@@ -6212,11 +6449,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => [$user2, $user3, $user4]
             ],
@@ -6227,11 +6466,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user6],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6242,11 +6483,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user7],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6257,11 +6500,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'arguments' => [$user8],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 1
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6347,6 +6592,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
         $this->assertEquals($expectedcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP],
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
+        $this->assertEquals($expectedcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF],
+            $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF]);
     }
 
     /**
@@ -6459,6 +6706,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
         $this->assertEquals($expectedunreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP],
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
+        $this->assertEquals($expectedunreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF],
+            $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF]);
     }
 
     public function test_delete_all_conversation_data() {
index d76e1ce..a87a181 100644 (file)
@@ -2719,6 +2719,36 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, $noncontacts[3]['conversations'][0]['type']);
     }
 
+    /**
+     * Verify searching for users find themselves when they have self-conversations.
+     */
+    public function test_message_search_users_self_conversations() {
+        $this->resetAfterTest();
+
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User';
+        $user1->lastname = 'One';
+        $user1 = $this->getDataGenerator()->create_user($user1);
+        $user2 = new stdClass();
+        $user2->firstname = 'User';
+        $user2->lastname = 'Two';
+        $user2 = $this->getDataGenerator()->create_user($user2);
+
+        // Create self-conversation for user1.
+        $sc1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, [$user1->id]);
+        testhelper::send_fake_message_to_conversation($user1, $sc1->id, 'Hi myself!');
+
+        // Perform a search as user1.
+        $this->setUser($user1);
+        $result = core_message_external::message_search_users($user1->id, 'One');
+        $result = external_api::clean_returnvalue(core_message_external::message_search_users_returns(), $result);
+
+        // Check results are empty.
+        $this->assertCount(0, $result['contacts']);
+        $this->assertCount(1, $result['noncontacts']);
+    }
+
     /**
      * Verify searching for users works even if no matching users from either contacts, or non-contacts can be found.
      */
@@ -5496,32 +5526,30 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
     }
 
     /**
-     * Tests retrieving conversations when a legacy 'self' conversation exists.
+     * Tests retrieving conversations when a 'self' conversation exists.
      */
-    public function test_get_conversations_legacy_self_conversations() {
+    public function test_get_conversations_self_conversations() {
         global $DB;
         $this->resetAfterTest();
 
-        // Create a legacy conversation between one user and themself.
+        // Create a conversation between one user and themself.
         $user1 = self::getDataGenerator()->create_user();
-        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
-            [$user1->id, $user1->id]);
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+            [$user1->id]);
         testhelper::send_fake_message_to_conversation($user1, $conversation->id, 'Test message to self!');
 
         // Verify we are in a 'self' conversation state.
         $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation->id]);
-        $this->assertCount(2, $members);
-        $member = array_pop($members);
-        $this->assertEquals($user1->id, $member->userid);
+        $this->assertCount(1, $members);
         $member = array_pop($members);
         $this->assertEquals($user1->id, $member->userid);
 
-        // Verify this conversation is not returned by the method.
+        // Verify this conversation is returned by the method.
         $this->setUser($user1);
         $result = core_message_external::get_conversations($user1->id, 0, 20);
         $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
         $conversations = $result['conversations'];
-        $this->assertCount(0, $conversations);
+        $this->assertCount(1, $conversations);
     }
 
     /**
@@ -6531,6 +6559,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
     public function test_get_conversation_counts_test_cases() {
         $typeindividual = \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL;
         $typegroup = \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP;
+        $typeself = \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF;
         list($user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8) = [0, 1, 2, 3, 4, 5, 6, 7];
         $conversations = [
             [
@@ -6569,9 +6598,9 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'enabled' => false
             ],
             [
-                'type' => $typeindividual,
-                'users' => [$user8, $user8],
-                'messages' => [$user8, $user8],
+                'type' => $typeself,
+                'users' => [$user8],
+                'messages' => [$user8],
                 'favourites' => [],
                 'enabled' => null // Individual conversations cannot be disabled.
             ],
@@ -6585,11 +6614,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user5],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6600,11 +6631,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user4],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6615,11 +6648,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6630,11 +6665,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user2],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6645,11 +6682,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user4],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6660,11 +6699,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6675,11 +6716,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6690,11 +6733,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6705,11 +6750,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6720,11 +6767,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user2],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6735,11 +6784,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user3],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6750,11 +6801,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6765,11 +6818,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6780,11 +6835,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => [$user2]
             ],
@@ -6795,11 +6852,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user1],
                 'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => [$user2, $user3, $user4]
             ],
@@ -6810,11 +6869,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user6],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6825,11 +6886,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user7],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6840,11 +6903,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'arguments' => [$user8],
                 'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 1
                 ]],
                 'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF => 0
                 ]],
                 'deletedusers' => []
             ],
@@ -6933,6 +6998,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
         $this->assertEquals($expectedcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP],
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
+        $this->assertEquals($expectedcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF],
+            $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF]);
     }
 
     /**
@@ -7017,5 +7084,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
         $this->assertEquals($expectedunreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP],
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
+        $this->assertEquals($expectedunreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF],
+            $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF]);
     }
 }
index c77781b..36ed008 100644 (file)
@@ -87,7 +87,18 @@ class core_message_messagelib_testcase extends advanced_testcase {
             return $DB->insert_record('notifications', $record);
         }
 
-        if (!$conversationid = \core_message\api::get_conversation_between_users([$userfrom->id, $userto->id])) {
+        if ($userfrom->id == $userto->id) {
+            // It's a self conversation.
+            $conversation = \core_message\api::get_self_conversation($userfrom->id);
+            if (empty($conversation)) {
+                $conversation = \core_message\api::create_conversation(
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+                    [$userfrom->id]
+                );
+            }
+            $conversationid = $conversation->id;
+        } else if (!$conversationid = \core_message\api::get_conversation_between_users([$userfrom->id, $userto->id])) {
+            // It's an individual conversation between two different users.
             $conversation = \core_message\api::create_conversation(
                 \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
                 [
index d220d00..e37e9e4 100644 (file)
@@ -9,6 +9,9 @@ information provided here is intended especially for developers.
   functionality and remove all the legacy code (see MDL-63915).
   Note - It's still possible to view another user's messages if you have the right capabilities and are able to
   'log-in as' them.
+* A new parameter 'mergeself' has been added to the methods \core_message\api::get_conversations() and
+  core_message_external::get_conversations(), to decide whether the self-conversations should be included or not when the
+  private ones are requested, to display them together.
 
 === 3.6 ===
 
index cb8e979..536fa20 100644 (file)
@@ -126,6 +126,10 @@ function user_create_user($user, $updatepassword = true, $triggerevent = true) {
         \core\event\user_created::create_from_userid($newuserid)->trigger();
     }
 
+    // All new users must have a starred self-conversation.
+    $selfconversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF, [$newuserid]);
+    \core_message\api::set_favourite_conversation($selfconversation->id, $newuserid);
+
     // Purge the associated caches for the current user only.
     $presignupcache = \cache::make('core', 'presignup');
     $presignupcache->purge_current_user();
index 9c67dd7..bad8a94 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2019041300.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2019041300.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.