Merge branch 'MDL-64956-master2' of git://github.com/peterRd/moodle
authorJun Pataleta <jun@moodle.com>
Thu, 18 Apr 2019 03:09:49 +0000 (11:09 +0800)
committerJun Pataleta <jun@moodle.com>
Thu, 18 Apr 2019 03:09:49 +0000 (11:09 +0800)
64 files changed:
admin/tool/log/classes/helper/reader.php
admin/tool/log/store/standard/tests/store_test.php
install/lang/fi/moodle.php
lang/en/message.php
lib/classes/message/manager.php
lib/db/services.php
lib/db/upgrade.php
lib/messagelib.php
lib/outputrenderers.php
lib/tests/messagelib_test.php
lib/upgrade.txt
message/amd/build/message_drawer.min.js
message/amd/build/message_drawer_router.min.js
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_drawer_view_search.min.js
message/amd/build/message_repository.min.js
message/amd/src/message_drawer.js
message/amd/src/message_drawer_router.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_drawer_view_search.js
message/amd/src/message_repository.js
message/classes/api.php
message/classes/helper.php
message/classes/privacy/provider.php
message/classes/task/migrate_message_data.php
message/externallib.php
message/index.php
message/templates/message_drawer_contacts_list.mustache
message/templates/message_drawer_icon_back.mustache
message/templates/message_drawer_messages_list.mustache
message/templates/message_drawer_non_contacts_list.mustache
message/templates/message_drawer_view_contacts_header.mustache
message/templates/message_drawer_view_conversation_body.mustache
message/templates/message_drawer_view_conversation_header_content_type_private.mustache
message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
message/templates/message_drawer_view_conversation_header_content_type_public.mustache
message/templates/message_drawer_view_conversation_header_content_type_self.mustache [new file with mode: 0644]
message/templates/message_drawer_view_conversation_header_placeholder.mustache
message/templates/message_drawer_view_overview_body.mustache
message/templates/message_drawer_view_overview_header.mustache
message/templates/message_drawer_view_search_body.mustache
message/templates/message_drawer_view_search_header.mustache
message/templates/message_drawer_view_settings_header.mustache
message/templates/message_index.mustache
message/tests/api_test.php
message/tests/externallib_test.php
message/tests/messagelib_test.php
message/upgrade.txt
mod/chat/tests/lib_test.php
theme/boost/scss/moodle/message.scss
theme/boost/style/moodle.css
theme/classic/style/moodle.css
user/lib.php
version.php

index cc7e8b8..2e94774 100644 (file)
@@ -73,7 +73,7 @@ trait reader {
      * @param string $other Other value
      * @return mixed Decoded value
      */
-    public static function decode_other(string $other) {
+    public static function decode_other(?string $other) {
         if ($other === 'N;' || preg_match('~^.:~', $other)) {
             return unserialize($other);
         } else {
index f2d0531..25963af 100644 (file)
@@ -374,6 +374,10 @@ class logstore_standard_store_testcase extends advanced_testcase {
         $this->assertEquals($value, \logstore_standard\log\store::decode_other(json_encode($value)));
     }
 
+    public function test_decode_other_with_wrongly_encoded_contents() {
+        $this->assertSame(null, \logstore_standard\log\store::decode_other(null));
+    }
+
     /**
      * List of possible values for 'other' field.
      *
index 3045e1e..4a2b637 100644 (file)
@@ -31,6 +31,7 @@
 defined('MOODLE_INTERNAL') || die();
 
 $string['language'] = 'Kieli';
+$string['moodlelogo'] = 'Moodlen logo';
 $string['next'] = 'Seuraava';
 $string['previous'] = 'Edellinen';
 $string['reload'] = 'Lataa uudelleen';
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 76dc143..76418fa 100644 (file)
@@ -2996,9 +2996,193 @@ function xmldb_main_upgrade($oldversion) {
         if (!$dbman->field_exists($table, $field)) {
             $dbman->add_field($table, $field);
         }
-
+        // Main savepoint reached.
         upgrade_main_savepoint(true, 2019041300.01);
     }
 
+    if ($oldversion < 2019041800.01) {
+        // STEP 1. For the existing and migrated self-conversations, set the type to the new MESSAGE_CONVERSATION_TYPE_SELF, update
+        // the convhash and star them.
+        $sql = "SELECT mcm.conversationid, mcm.userid, MAX(mcm.id) as maxid
+                  FROM {message_conversation_members} mcm
+              GROUP BY mcm.conversationid, mcm.userid
+                HAVING COUNT(*) > 1";
+        $selfconversationsrs = $DB->get_recordset_sql($sql);
+        $maxids = [];
+        foreach ($selfconversationsrs as $selfconversation) {
+            $DB->update_record('message_conversations',
+                ['id' => $selfconversation->conversationid,
+                 'type' => \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+                 'convhash' => \core_message\helper::get_conversation_hash([$selfconversation->userid])
+                ]
+            );
+
+            // Star the existing self-conversation.
+            $favouriterecord = new \stdClass();
+            $favouriterecord->component = 'core_message';
+            $favouriterecord->itemtype = 'message_conversations';
+            $favouriterecord->itemid = $selfconversation->conversationid;
+            $userctx = \context_user::instance($selfconversation->userid);
+            $favouriterecord->contextid = $userctx->id;
+            $favouriterecord->userid = $selfconversation->userid;
+            $favouriterecord->timecreated = time();
+            $favouriterecord->timemodified = $favouriterecord->timecreated;
+
+            $DB->insert_record('favourite', $favouriterecord);
+
+            // Set the self-conversation member with maxid to remove it later.
+            $maxids[] = $selfconversation->maxid;
+        }
+        $selfconversationsrs->close();
+
+        // Remove the repeated member with the higher id for all the existing self-conversations.
+        if (!empty($maxids)) {
+            list($insql, $inparams) = $DB->get_in_or_equal($maxids);
+            $DB->delete_records_select('message_conversation_members', "id $insql", $inparams);
+        }
+
+        // STEP 2. Migrate existing self-conversation relying on old message tables, setting the type to the new
+        // MESSAGE_CONVERSATION_TYPE_SELF and the convhash to the proper one. Star them also.
+
+        // On the messaging legacy tables, self-conversations are only present in the 'message_read' table, so we don't need to
+        // check the content in the 'message' table.
+        $select = 'useridfrom = useridto AND notification = 0';
+        $legacyselfmessagesrs = $DB->get_recordset_select('message_read', $select);
+        foreach ($legacyselfmessagesrs as $message) {
+            // Get the self-conversation or create and star it if doesn't exist.
+            $conditions = [
+                'type' => \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+                'convhash' => \core_message\helper::get_conversation_hash([$message->useridfrom])
+            ];
+            $selfconversation = $DB->get_record('message_conversations', $conditions);
+            if (empty($selfconversation)) {
+                // Create the self-conversation.
+                $selfconversation = new \stdClass();
+                $selfconversation->type = \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF;
+                $selfconversation->convhash = \core_message\helper::get_conversation_hash([$message->useridfrom]);
+                $selfconversation->enabled = 1;
+                $selfconversation->timecreated = time();
+                $selfconversation->timemodified = $selfconversation->timecreated;
+
+                $selfconversation->id = $DB->insert_record('message_conversations', $selfconversation);
+
+                // Add user to this self-conversation.
+                $member = new \stdClass();
+                $member->conversationid = $selfconversation->id;
+                $member->userid = $message->useridfrom;
+                $member->timecreated = time();
+
+                $member->id = $DB->insert_record('message_conversation_members', $member);
+
+                // Star the self-conversation.
+                $favouriterecord = new \stdClass();
+                $favouriterecord->component = 'core_message';
+                $favouriterecord->itemtype = 'message_conversations';
+                $favouriterecord->itemid = $selfconversation->id;
+                $userctx = \context_user::instance($message->useridfrom);
+                $favouriterecord->contextid = $userctx->id;
+                $favouriterecord->userid = $message->useridfrom;
+                $favouriterecord->timecreated = time();
+                $favouriterecord->timemodified = $favouriterecord->timecreated;
+
+                $DB->insert_record('favourite', $favouriterecord);
+            }
+
+            // Create the object we will be inserting into the database.
+            $tabledata = new \stdClass();
+            $tabledata->useridfrom = $message->useridfrom;
+            $tabledata->conversationid = $selfconversation->id;
+            $tabledata->subject = $message->subject;
+            $tabledata->fullmessage = $message->fullmessage;
+            $tabledata->fullmessageformat = $message->fullmessageformat ?? FORMAT_MOODLE;
+            $tabledata->fullmessagehtml = $message->fullmessagehtml;
+            $tabledata->smallmessage = $message->smallmessage;
+            $tabledata->timecreated = $message->timecreated;
+
+            $messageid = $DB->insert_record('messages', $tabledata);
+
+            // Check if we need to mark this message as deleted (self-conversations add this information on the
+            // timeuserfromdeleted field.
+            if ($message->timeuserfromdeleted) {
+                $mua = new \stdClass();
+                $mua->userid = $message->useridfrom;
+                $mua->messageid = $messageid;
+                $mua->action = \core_message\api::MESSAGE_ACTION_DELETED;
+                $mua->timecreated = $message->timeuserfromdeleted;
+
+                $DB->insert_record('message_user_actions', $mua);
+            }
+
+            // Mark this message as read.
+            $mua = new \stdClass();
+            $mua->userid = $message->useridto;
+            $mua->messageid = $messageid;
+            $mua->action = \core_message\api::MESSAGE_ACTION_READ;
+            $mua->timecreated = $message->timeread;
+
+            $DB->insert_record('message_user_actions', $mua);
+        }
+        $legacyselfmessagesrs->close();
+
+        // We can now delete the records from legacy table because the self-conversations have been migrated from the legacy tables.
+        $DB->delete_records_select('message_read', $select);
+
+        // STEP 3. For existing users without self-conversations, create and star it.
+
+        // Get all the users without a self-conversation.
+        $sql = "SELECT u.id
+                  FROM {user} u
+                  WHERE u.id NOT IN (SELECT mcm.userid
+                                     FROM {message_conversation_members} mcm
+                                     INNER JOIN mdl_message_conversations mc
+                                             ON mc.id = mcm.conversationid AND mc.type = ?
+                                    )";
+        $useridsrs = $DB->get_recordset_sql($sql, [\core_message\api::MESSAGE_CONVERSATION_TYPE_SELF]);
+        // Create the self-conversation for all these users.
+        foreach ($useridsrs as $user) {
+            $conditions = [
+                'type' => \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
+                'convhash' => \core_message\helper::get_conversation_hash([$user->id])
+            ];
+            $selfconversation = $DB->get_record('message_conversations', $conditions);
+            if (empty($selfconversation)) {
+                // Create the self-conversation.
+                $selfconversation = new \stdClass();
+                $selfconversation->type = \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF;
+                $selfconversation->convhash = \core_message\helper::get_conversation_hash([$user->id]);
+                $selfconversation->enabled = 1;
+                $selfconversation->timecreated = time();
+                $selfconversation->timemodified = $selfconversation->timecreated;
+
+                $selfconversation->id = $DB->insert_record('message_conversations', $selfconversation);
+
+                // Add user to this self-conversation.
+                $member = new \stdClass();
+                $member->conversationid = $selfconversation->id;
+                $member->userid = $user->id;
+                $member->timecreated = time();
+
+                $member->id = $DB->insert_record('message_conversation_members', $member);
+
+                // Star the self-conversation.
+                $favouriterecord = new \stdClass();
+                $favouriterecord->component = 'core_message';
+                $favouriterecord->itemtype = 'message_conversations';
+                $favouriterecord->itemid = $selfconversation->id;
+                $userctx = \context_user::instance($user->id);
+                $favouriterecord->contextid = $userctx->id;
+                $favouriterecord->userid = $user->id;
+                $favouriterecord->timecreated = time();
+                $favouriterecord->timemodified = $favouriterecord->timecreated;
+
+                $DB->insert_record('favourite', $favouriterecord);
+            }
+        }
+        $useridsrs->close();
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2019041800.01);
+    }
+
     return true;
 }
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 27ee12d..4845832 100644 (file)
@@ -4090,11 +4090,7 @@ EOD;
                 $imagedata = $this->user_picture($user, array('size' => 100));
 
                 // Check to see if we should be displaying a message button.
-                if (!empty($CFG->messaging) && $USER->id != $user->id && has_capability('moodle/site:sendmessage', $context)) {
-                    $iscontact = \core_message\api::is_contact($USER->id, $user->id);
-                    $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
-                    $contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
-                    $contactimage = $iscontact ? 'removecontact' : 'addcontact';
+                if (!empty($CFG->messaging) && has_capability('moodle/site:sendmessage', $context)) {
                     $userbuttons = array(
                         'messages' => array(
                             'buttontype' => 'message',
@@ -4103,22 +4099,29 @@ EOD;
                             'image' => 'message',
                             'linkattributes' => \core_message\helper::messageuser_link_params($user->id),
                             'page' => $this->page
-                        ),
-                        'togglecontact' => array(
-                            'buttontype' => 'togglecontact',
-                            'title' => get_string($contacttitle, 'message'),
-                            'url' => new moodle_url('/message/index.php', array(
-                                    'user1' => $USER->id,
-                                    'user2' => $user->id,
-                                    $contacturlaction => $user->id,
-                                    'sesskey' => sesskey())
-                            ),
-                            'image' => $contactimage,
-                            'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact),
-                            'page' => $this->page
-                        ),
+                        )
                     );
 
+                    if ($USER->id != $user->id) {
+                        $iscontact = \core_message\api::is_contact($USER->id, $user->id);
+                        $contacttitle = $iscontact ? 'removefromyourcontacts' : 'addtoyourcontacts';
+                        $contacturlaction = $iscontact ? 'removecontact' : 'addcontact';
+                        $contactimage = $iscontact ? 'removecontact' : 'addcontact';
+                        $userbuttons['togglecontact'] = array(
+                                'buttontype' => 'togglecontact',
+                                'title' => get_string($contacttitle, 'message'),
+                                'url' => new moodle_url('/message/index.php', array(
+                                        'user1' => $USER->id,
+                                        'user2' => $user->id,
+                                        $contacturlaction => $user->id,
+                                        'sesskey' => sesskey())
+                                ),
+                                'image' => $contactimage,
+                                'linkattributes' => \core_message\helper::togglecontact_link_params($user, $iscontact),
+                                'page' => $this->page
+                            );
+                    }
+
                     $this->page->requires->string_for_js('changesmadereallygoaway', 'moodle');
                 }
             } else {
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 d0baed6..5207a6e 100644 (file)
@@ -31,6 +31,14 @@ attribute on forms to avoid collisions in forms loaded in AJAX requests.
 * It is possible to pass additional conditions to get_courses_search();
   core_course_category::search_courses() now allows to search only among courses with completion enabled.
 * Add support for a new xxx_after_require_login callback
+* A new conversation type has been created for self-conversations. During the upgrading process:
+  - Firstly, the existing self-conversations will be starred and migrated to the new type, removing the duplicated members in the
+  message_conversation_members table.
+  - Secondly, the legacy self conversations will be migrated from the legacy 'message_read' table. They will be created using the
+  new conversation type and will be favourited.
+  - Finally, the self-conversations for all remaining users without them will be created and starred.
+Besides, from now, a self-conversation will be created and starred by default to all the new users (even when $CFG->messaging
+is disabled).
 
 === 3.6 ===
 
index 453da2f..9f89d03 100644 (file)
Binary files a/message/amd/build/message_drawer.min.js and b/message/amd/build/message_drawer.min.js differ
index ef9875e..a135ac2 100644 (file)
Binary files a/message/amd/build/message_drawer_router.min.js and b/message/amd/build/message_drawer_router.min.js differ
index 6d5e118..21837f5 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..2c2761d 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..1fdea1f 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..9646cb8 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..c6c52b6 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..7fb488b 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 2f3881e..071d79e 100644 (file)
Binary files a/message/amd/build/message_drawer_view_search.min.js and b/message/amd/build/message_drawer_view_search.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 8823ac0..9a800c5 100644 (file)
@@ -53,6 +53,8 @@ function(
 ) {
 
     var SELECTORS = {
+        PANEL_BODY_CONTAINER: '[data-region="panel-body-container"]',
+        PANEL_HEADER_CONTAINER: '[data-region="panel-header-container"]',
         VIEW_CONTACT: '[data-region="view-contact"]',
         VIEW_CONTACTS: '[data-region="view-contacts"]',
         VIEW_CONVERSATION: '[data-region="view-conversation"]',
@@ -77,10 +79,16 @@ function(
      * @return {array} elements Found route container objects.
     */
     var getParametersForRoute = function(namespace, root, selector) {
-        var candidates = root.children();
-        var header = candidates.filter(SELECTORS.HEADER_CONTAINER).find(selector);
-        var body = candidates.filter(SELECTORS.BODY_CONTAINER).find(selector);
-        var footer = candidates.filter(SELECTORS.FOOTER_CONTAINER).find(selector);
+
+        var header = root.find(SELECTORS.HEADER_CONTAINER).find(selector);
+        if (!header.length) {
+            header = root.find(SELECTORS.PANEL_HEADER_CONTAINER).find(selector);
+        }
+        var body = root.find(SELECTORS.BODY_CONTAINER).find(selector);
+        if (!body.length) {
+            body = root.find(SELECTORS.PANEL_BODY_CONTAINER).find(selector);
+        }
+        var footer = root.find(SELECTORS.FOOTER_CONTAINER).find(selector);
 
         return [
             namespace,
@@ -193,6 +201,7 @@ function(
             var params = paramAttributes.map(function(attribute) {
                 return attribute.nodeValue;
             });
+
             var routeParams = [namespace, route].concat(params);
 
             Router.go.apply(null, routeParams);
index 5134ee6..a03b86c 100644 (file)
@@ -80,6 +80,9 @@ function(
      */
     var changeRoute = function(namespace, newRoute) {
         var newConfig;
+
+        // Check of the Route change call is made from an element in the app panel.
+        var fromPanel = [].slice.call(arguments).includes('frompanel');
         // Get the rest of the arguments, if any.
         var args = [].slice.call(arguments, 2);
         var renderPromise = $.Deferred().resolve().promise();
@@ -99,13 +102,24 @@ function(
                 }
 
                 element.removeClass('previous');
+                element.attr('data-from-panel', false);
 
                 if (isMatch) {
+                    if (fromPanel) {
+                        // Set this attribute to let the conversation renderer know not to show a back button.
+                        element.attr('data-from-panel', true);
+                    }
                     element.removeClass('hidden');
                     element.attr('aria-hidden', false);
                 } else {
-                    element.addClass('hidden');
-                    element.attr('aria-hidden', true);
+                    // For the message index page elements in the left panel should not be hidden.
+                    if (!element.attr('data-in-panel')) {
+                        element.addClass('hidden');
+                        element.attr('aria-hidden', true);
+                    } else if (newRoute == 'view-search' || newRoute == 'view-overview') {
+                        element.addClass('hidden');
+                        element.attr('aria-hidden', true);
+                    }
                 }
             });
         });
@@ -163,7 +177,8 @@ function(
      */
     var go = function(namespace) {
         var currentFocusElement = $(document.activeElement);
-        var record = changeRoute.apply(null, arguments);
+
+        var record = changeRoute.apply(namespace, arguments);
         var inHistory = false;
 
         if (!history[namespace]) {
@@ -222,7 +237,6 @@ function(
                                 if (typeof element !== 'object' || !element) {
                                     return;
                                 }
-
                                 // Update the aria label for the back button.
                                 element.find(SELECTORS.ROUTES_BACK).attr('aria-label', label);
                             });
index 8b8949b..1b4050b 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.PUBLIC) {
             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.PUBLIC) {
                     if (userId in state.members) {
                         // We've found a cached conversation for this user!
                         carry = state.id;
@@ -269,6 +274,9 @@ function(
      */
     var loadEmptyPrivateConversation = function(loggedInUserProfile, otherUserId) {
         var loggedInUserId = loggedInUserProfile.id;
+        // If the other user id is the same as the logged in user then this is a self
+        // conversation.
+        var conversationType = loggedInUserId == otherUserId ? CONVERSATION_TYPES.SELF : CONVERSATION_TYPES.PRIVATE;
         var newState = StateManager.setLoadingMembers(viewState, true);
         newState = StateManager.setLoadingMessages(newState, true);
         return render(newState)
@@ -283,13 +291,16 @@ function(
                 }
             })
             .then(function(profile) {
-                var newState = StateManager.addMembers(viewState, [profile, loggedInUserProfile]);
+                // If the conversation is a self conversation then the profile loaded is the
+                // logged in user so only add that to the members array.
+                var members = conversationType == CONVERSATION_TYPES.SELF ? [profile] : [profile, loggedInUserProfile];
+                var newState = StateManager.addMembers(viewState, members);
                 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, conversationType);
                 newState = StateManager.setImageUrl(newState, profile.profileimageurl);
-                newState = StateManager.setTotalMemberCount(newState, 2);
+                newState = StateManager.setTotalMemberCount(newState, members.length);
                 return render(newState)
                     .then(function() {
                         return profile;
@@ -310,14 +321,22 @@ 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.PUBLIC) {
             name = name || otherUser ? otherUser.fullname : '';
             imageUrl = imageUrl || otherUser ? otherUser.profileimageurl : '';
         }
@@ -921,6 +940,7 @@ function(
                 newState = StateManager.setPendingDeleteConversation(newState, false);
                 newState = StateManager.setLoadingConfirmAction(newState, false);
                 PubSub.publish(MessageDrawerEvents.CONVERSATION_DELETED, newState.id);
+
                 return render(newState);
             });
     };
@@ -1019,7 +1039,7 @@ function(
         var newConversationId = null;
         return render(newState)
             .then(function() {
-                if (!conversationId && viewState.type == CONVERSATION_TYPES.PRIVATE) {
+                if (!conversationId && (viewState.type != CONVERSATION_TYPES.PUBLIC)) {
                     // 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 +1491,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.
@@ -1483,7 +1503,9 @@ function(
         // state manager and patcher can work correctly.
         return resetState(body, null, loggedInUserProfile)
             .then(function() {
-                return Repository.getConversationBetweenUsers(
+                if (loggedInUserProfile.id != otherUserId) {
+                    // Private conversation between two different users.
+                    return Repository.getConversationBetweenUsers(
                         loggedInUserProfile.id,
                         otherUserId,
                         true,
@@ -1493,15 +1515,24 @@ function(
                         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 {
+                    // Self conversation.
+                    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 loadEmptyPrivateConversation(loggedInUserProfile, otherUserId);
             });
     };
 
index 3bd84c8..04b6584 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,14 +89,17 @@ 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
     };
 
     return {
index 43ea13c..1d59260 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 && newState.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,23 @@ 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) {
+        if (state.type != newState.type) {
+            return (newState.type == Constants.CONVERSATION_TYPES.SELF);
+        }
+
+        return null;
+    };
+
     /**
      * 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 +1243,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..ab213b7 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.
      *
@@ -780,9 +800,11 @@ function(
     var renderHeader = function(header, body, footer, data) {
         var headerContainer = getHeaderContent(header);
         var template = TEMPLATES.HEADER_PUBLIC;
-
+        data.context.showrouteback = (header.attr('data-from-panel') === "false");
         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..8b6e979 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,9 +55,11 @@ function(
         SECTION_TOGGLE_BUTTON: '[data-toggle]'
     };
 
-    var CONVERSATION_TYPES = {
-        PRIVATE: 1,
-        PUBLIC: 2,
+    // 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 OVERVIEW_SECTION_TYPES = {
+        PRIVATE: [Constants.CONVERSATION_TYPES.PRIVATE, Constants.CONVERSATION_TYPES.SELF],
+        PUBLIC: [Constants.CONVERSATION_TYPES.PUBLIC],
         FAVOURITE: null
     };
 
@@ -85,11 +89,24 @@ function(
      * This is used on the result returned by the loadAllCounts function.
      *
      * @param {Object} counts Conversation counts indexed by conversation type.
-     * @param {String|null} type The conversation type (null for favourites only).
+     * @param {Array|null} types The conversation types handlded by this section (null for all conversation types).
+     * @param {bool} includeFavourites If this section includes favourites
      * @return {Number}
      */
-    var filterCountsByType = function(counts, type) {
-        return type === CONVERSATION_TYPES.FAVOURITE ? counts.favourites : counts.types[type];
+    var filterCountsByTypes = function(counts, types, includeFavourites) {
+        var total = 0;
+
+        if (types && types.length) {
+            total = types.reduce(function(carry, type) {
+                return carry + counts.types[type];
+            }, total);
+        }
+
+        if (includeFavourites) {
+            total += counts.favourites;
+        }
+
+        return total;
     };
 
     /**
@@ -219,6 +236,7 @@ function(
             registerEventListeners(namespace, header);
             header.attr('data-init', true);
         }
+        var fromPanel = header.attr('data-in-panel') ? 'frompanel' : null;
 
         getSearchInput(header).val('');
         var loggedInUserId = getLoggedInUserId(body);
@@ -226,34 +244,35 @@ function(
 
         var sections = [
             // Favourite conversations section.
-            [body.find(SELECTORS.FAVOURITES), CONVERSATION_TYPES.FAVOURITE, true],
+            [body.find(SELECTORS.FAVOURITES), OVERVIEW_SECTION_TYPES.FAVOURITE, true],
             // Group conversations section.
-            [body.find(SELECTORS.GROUP_MESSAGES), CONVERSATION_TYPES.PUBLIC, false],
+            [body.find(SELECTORS.GROUP_MESSAGES), OVERVIEW_SECTION_TYPES.PUBLIC, false],
             // Private conversations section.
-            [body.find(SELECTORS.MESSAGES), CONVERSATION_TYPES.PRIVATE, false]
+            [body.find(SELECTORS.MESSAGES), OVERVIEW_SECTION_TYPES.PRIVATE, false]
         ];
 
         sections.forEach(function(args) {
             var sectionRoot = args[0];
-            var sectionType = args[1];
+            var sectionTypes = args[1];
             var includeFavourites = args[2];
             var totalCountPromise = allCounts.then(function(result) {
-                return filterCountsByType(result.total, sectionType);
+                return filterCountsByTypes(result.total, sectionTypes, includeFavourites);
             });
             var unreadCountPromise = allCounts.then(function(result) {
-                return filterCountsByType(result.unread, sectionType);
+                return filterCountsByTypes(result.unread, sectionTypes, includeFavourites);
             });
 
-            Section.show(namespace, null, sectionRoot, null, sectionType, includeFavourites,
-                totalCountPromise, unreadCountPromise);
+            Section.show(namespace, null, sectionRoot, null, sectionTypes, includeFavourites,
+                totalCountPromise, unreadCountPromise, fromPanel);
         });
 
         return allCounts.then(function(result) {
                 var sectionParams = sections.map(function(section) {
                     var sectionRoot = section[0];
-                    var sectionType = section[1];
-                    var totalCount = filterCountsByType(result.total, sectionType);
-                    var unreadCount = filterCountsByType(result.unread, sectionType);
+                    var sectionTypes = section[1];
+                    var includeFavourites = section[2];
+                    var totalCount = filterCountsByTypes(result.total, sectionTypes, includeFavourites);
+                    var unreadCount = filterCountsByTypes(result.unread, sectionTypes, includeFavourites);
 
                     return [sectionRoot, totalCount, unreadCount];
                 });
index 01dba3e..eafd6ca 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;
@@ -226,19 +233,42 @@ function(
     /**
      * Build the callback to load conversations.
      *
-     * @param  {Number} type The conversation type.
+     * @param  {Array|null} types The conversation types for this section.
      * @param  {bool} includeFavourites Include/exclude favourites.
      * @param  {Number} offset Result offset
      * @return {Function}
      */
-    var getLoadCallback = function(type, includeFavourites, offset) {
+    var getLoadCallback = function(types, includeFavourites, offset) {
+        // Note: This function is a bit messy because we've added the concept of loading
+        // multiple conversations types (e.g. private + self) at once but haven't properly
+        // updated the web service to accept an array of types. Instead we've added a new
+        // parameter for the self type which means we can only ever load self + other type.
+        // This should be improved to make it more extensible in the future. Adding new params
+        // for each type isn't very scalable.
+        var type = null;
+        // Include self conversations in the results by default.
+        var includeSelfConversations = true;
+        if (types && types.length) {
+            // Just get the conversation types that aren't "self" for now.
+            var nonSelfConversationTypes = types.filter(function(candidate) {
+                return candidate != MessageDrawerViewConversationContants.CONVERSATION_TYPES.SELF;
+            });
+            // If we're specifically asking for a list of types that doesn't include the self
+            // conversations then we don't need to include them.
+            includeSelfConversations = types.length != nonSelfConversationTypes.length;
+            // As mentioned above the webservice is currently limited to loading one type at a
+            // time (plus self conversations) so let's hope we never change this.
+            type = nonSelfConversationTypes[0];
+        }
+
         return function(root, userId) {
             return MessageRepository.getConversations(
                     userId,
                     type,
                     LOAD_LIMIT + 1,
                     offset,
-                    includeFavourites
+                    includeFavourites,
+                    includeSelfConversations
                 )
                 .then(function(response) {
                     var conversations = response.conversations;
@@ -523,11 +553,28 @@ function(
      * @param {String} namespace Unique identifier for the Routes
      * @param {Object} root The section container element.
      * @param {Function} loadCallback The callback to load items.
-     * @param {Number} type The conversation type for this section
+     * @param {Array|null} types The conversation types for this section
      * @param {bool} includeFavourites If this section includes favourites
+     * @param {String} fromPanel Routing argument to send if the section is loaded in message index left panel.
      */
-    var registerEventListeners = function(namespace, root, loadCallback, type, includeFavourites) {
+    var registerEventListeners = function(namespace, root, loadCallback, types, includeFavourites, fromPanel) {
         var listRoot = LazyLoadList.getRoot(root);
+        var conversationBelongsToThisSection = function(conversation) {
+            // Make sure the type is an int so that the index of check matches correctly.
+            var conversationType = parseInt(conversation.type, 10);
+            if (
+                // If the conversation type isn't one this section cares about then we can ignore it.
+                (types && types.indexOf(conversationType) < 0) ||
+                // If this is the favourites section and the conversation isn't a favourite then ignore it.
+                (includeFavourites && !conversation.isFavourite) ||
+                // If this section doesn't include favourites and the conversation is a favourite then ignore it.
+                (!includeFavourites && conversation.isFavourite)
+            ) {
+                return false;
+            }
+
+            return true;
+        };
 
         // Set the minimum height of the section to the height of the toggle. This
         // smooths out the collapse animation.
@@ -575,11 +622,7 @@ function(
         });
 
         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, function(conversation) {
-            if (
-                (type && conversation.type != type) ||
-                (includeFavourites && !conversation.isFavourite) ||
-                (!includeFavourites && conversation.isFavourite)
-            ) {
+            if (!conversationBelongsToThisSection(conversation)) {
                 return;
             }
 
@@ -608,12 +651,12 @@ function(
 
         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_FAVOURITE, function(conversation) {
             var conversationElement = null;
-            if (includeFavourites && (!type || type == conversation.type)) {
+            if (conversationBelongsToThisSection(conversation)) {
                 conversationElement = getConversationElement(root, conversation.id);
                 if (!conversationElement.length) {
                     createNewConversation(root, conversation);
                 }
-            } else if (type == conversation.type) {
+            } else {
                 conversationElement = getConversationElement(root, conversation.id);
                 if (conversationElement.length) {
                     deleteConversation(root, conversationElement);
@@ -623,16 +666,16 @@ function(
 
         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_FAVOURITE, function(conversation) {
             var conversationElement = null;
-            if (includeFavourites) {
-                conversationElement = getConversationElement(root, conversation.id);
-                if (conversationElement.length) {
-                    deleteConversation(root, conversationElement);
-                }
-            } else if (type == conversation.type) {
+            if (conversationBelongsToThisSection(conversation)) {
                 conversationElement = getConversationElement(root, conversation.id);
                 if (!conversationElement.length) {
                     createNewConversation(root, conversation);
                 }
+            } else {
+                conversationElement = getConversationElement(root, conversation.id);
+                if (conversationElement.length) {
+                    deleteConversation(root, conversationElement);
+                }
             }
         });
 
@@ -641,7 +684,7 @@ function(
             var conversationElement = $(e.target).closest(SELECTORS.CONVERSATION);
             var conversationId = conversationElement.attr('data-conversation-id');
             var conversation = loadedConversationsById[conversationId];
-            MessageDrawerRouter.go(namespace, MessageDrawerRoutes.VIEW_CONVERSATION, conversation);
+            MessageDrawerRouter.go(namespace, MessageDrawerRoutes.VIEW_CONVERSATION, conversation, fromPanel);
 
             data.originalEvent.preventDefault();
         });
@@ -654,17 +697,19 @@ function(
      * @param {Object} header The header container element.
      * @param {Object} body The section container element.
      * @param {Object} footer The footer container element.
-     * @param {Number} type The conversation type for this section
+     * @param {Array} types The conversation types that show in this section
      * @param {bool} includeFavourites If this section includes favourites
      * @param {Object} totalCountPromise Resolves wth the total conversations count
      * @param {Object} unreadCountPromise Resolves wth the unread conversations count
+     * @param {bool} fromPanel shown in message app panel.
      */
-    var show = function(namespace, header, body, footer, type, includeFavourites, totalCountPromise, unreadCountPromise) {
+    var show = function(namespace, header, body, footer, types, includeFavourites, totalCountPromise, unreadCountPromise,
+        fromPanel) {
         var root = $(body);
 
         if (!root.attr('data-init')) {
-            var loadCallback = getLoadCallback(type, includeFavourites, 0);
-            registerEventListeners(namespace, root, loadCallback, type, includeFavourites);
+            var loadCallback = getLoadCallback(types, includeFavourites, 0);
+            registerEventListeners(namespace, root, loadCallback, types, includeFavourites, fromPanel);
 
             if (isVisible(root)) {
                 setExpanded(root);
index 8db0aa7..8113f99 100644 (file)
@@ -621,9 +621,10 @@ function(
      */
     var renderContacts = function(root, contacts) {
         var container = getContactsContainer(root);
+        var frompanel = root.attr('data-in-panel');
         var list = container.find(SELECTORS.LIST);
 
-        return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: contacts})
+        return Templates.render(TEMPLATES.CONTACTS_LIST, {contacts: contacts, frompanel: frompanel})
             .then(function(html) {
                 list.append(html);
                 return html;
@@ -639,9 +640,10 @@ function(
      */
     var renderNonContacts = function(root, nonContacts) {
         var container = getNonContactsContainer(root);
+        var frompanel = root.attr('data-in-panel');
         var list = container.find(SELECTORS.LIST);
 
-        return Templates.render(TEMPLATES.NON_CONTACTS_LIST, {noncontacts: nonContacts})
+        return Templates.render(TEMPLATES.NON_CONTACTS_LIST, {noncontacts: nonContacts, frompanel: frompanel})
             .then(function(html) {
                 list.append(html);
                 return html;
@@ -657,9 +659,10 @@ function(
      */
     var renderMessages = function(root, messages) {
         var container = getMessagesContainer(root);
+        var frompanel = root.attr('data-in-panel');
         var list = container.find(SELECTORS.LIST);
 
-        return Templates.render(TEMPLATES.MESSAGES_LIST, {messages: messages})
+        return Templates.render(TEMPLATES.MESSAGES_LIST, {messages: messages, frompanel: frompanel})
             .then(function(html) {
                 list.append(html);
                 return html;
@@ -945,7 +948,6 @@ function(
             registerEventListeners(header, body);
             body.attr('data-init', true);
         }
-
         var searchInput = getSearchInput(header);
         searchInput.focus();
 
@@ -959,6 +961,9 @@ function(
      * @return {Object} jQuery promise
      */
     var description = function(header) {
+        if (typeof header !== 'object') {
+            return Str.get_string('messagedrawerviewsearch', 'core_message');
+        }
         var searchInput = getSearchInput(header);
         var searchText = searchInput.val().trim();
         return Str.get_string('messagedrawerviewsearch', 'core_message', searchText);
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 5bcf9f1..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.
      */
@@ -103,10 +108,12 @@ class api {
         // Get the user fields we want.
         $ufields = \user_picture::fields('u', array('lastaccess'), 'userfrom_id', 'userfrom_');
         $ufields2 = \user_picture::fields('u2', array('lastaccess'), 'userto_id', 'userto_');
+        // Add the uniqueid column to make each row unique and avoid SQL errors.
+        $uniqueidsql = $DB->sql_concat('m.id', "'_'", 'm.useridfrom', "'_'", 'mcm.userid');
 
-        $sql = "SELECT m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage, m.fullmessagehtml, m.fullmessageformat,
-                       m.smallmessage, m.conversationid, m.timecreated, 0 as isread, $ufields, mub.id as userfrom_blocked,
-                       $ufields2, mub2.id as userto_blocked
+        $sql = "SELECT $uniqueidsql AS uniqueid, m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage,
+                       m.fullmessagehtml, m.fullmessageformat, m.smallmessage, m.conversationid, m.timecreated, 0 as isread,
+                       $ufields, mub.id as userfrom_blocked, $ufields2, mub2.id as userto_blocked
                   FROM {messages} m
             INNER JOIN {user} u
                     ON u.id = m.useridfrom
@@ -123,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();
@@ -146,8 +154,13 @@ class api {
                 $message->blocked = $message->$blockedcol ? 1 : 0;
 
                 $message->messageid = $message->id;
-                $conversations[] = helper::create_contact($message, $prefix);
+                // To avoid duplicate messages, only add the message if it hasn't been added previously.
+                if (!array_key_exists($message->messageid, $conversations)) {
+                    $conversations[$message->messageid] = helper::create_contact($message, $prefix);
+                }
             }
+            // Remove the messageid keys (to preserve the expected type).
+            $conversations = array_values($conversations);
         }
 
         return $conversations;
@@ -309,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);
@@ -420,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;
+                }
             }
         }
 
@@ -507,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.");
         }
 
@@ -546,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,
@@ -580,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] = [];
@@ -614,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.
 
@@ -640,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.
@@ -658,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:
@@ -685,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.
@@ -693,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 [];
@@ -759,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'));
@@ -766,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;
             }
 
@@ -916,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,
@@ -1563,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
@@ -1575,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
@@ -1606,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";
@@ -1622,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.
@@ -1629,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;
@@ -1855,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.
@@ -2375,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.
      *
@@ -2413,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)) {
@@ -2425,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;
@@ -2833,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;
         }
 
@@ -3139,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 a1099b9..07d4ca9 100644 (file)
@@ -758,7 +758,8 @@ class helper {
                 'notification' => $notification
             ],
             'sendtouser' => false,
-            'conversationid' => false
+            'conversationid' => false,
+            'isdrawer' => $isdrawer
         ];
 
         if ($sendtouser) {
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 5883633..949685d 100644 (file)
@@ -59,7 +59,7 @@ if ($userid) {
 }
 $PAGE->set_url($url);
 $PAGE->set_context(context_user::instance($USER->id));
-$PAGE->set_pagelayout('standard');
+$PAGE->set_pagelayout('mydashboard');
 
 $strmessages = get_string('messages', 'message');
 
@@ -86,6 +86,5 @@ if (!get_user_preferences('core_message_migrate_data', false)) {
         \core\output\notification::NOTIFY_WARNING);
     echo $OUTPUT->render($notify);
 }
-echo $OUTPUT->heading(get_string('messages', 'message'));
 echo \core_message\helper::render_messaging_widget(false, $userid, $conversationid);
 echo $OUTPUT->footer();
index 8d24769..a9d96f5 100644 (file)
         data-route="view-conversation"
         {{#conversationid}}
             data-route-param-1="{{.}}"
+            {{#frompanel}}
+                data-route-param-2="frompanel"
+            {{/frompanel}}
         {{/conversationid}}
         {{^conversationid}}
             data-route-param-1="false"
             data-route-param-2="create"
             data-route-param-3="{{id}}"
+            {{#frompanel}}
+                data-route-param-4="frompanel"
+            {{/frompanel}}
         {{/conversationid}}
         data-contact-user-id="{{id}}"
         data-region="contact"
index d921970..3c533f1 100644 (file)
     {}
 
 }}
-<span class="dir-rtl-hide">{{#pix}} i/previous, core {{/pix}}</span>
-<span class="dir-ltr-hide">{{#pix}} i/next, core {{/pix}}</span>
\ No newline at end of file
+<div class="icon-back-in-drawer">
+    <span class="dir-rtl-hide">{{#pix}} i/previous, core {{/pix}}</span>
+    <span class="dir-ltr-hide">{{#pix}} i/next, core {{/pix}}</span>
+</div>
+<div class="icon-back-in-app">
+    <span class="dir-rtl-hide">{{#pix}} e/cancel, core {{/pix}}</span>
+</div>
\ No newline at end of file
index 97332f7..f623631 100644 (file)
         data-route="view-conversation"
         {{#conversationid}}
             data-route-param-1="{{.}}"
+            {{#frompanel}}
+                data-route-param-2="frompanel"
+            {{/frompanel}}
         {{/conversationid}}
         {{^conversationid}}
             data-route-param-1="false"
             data-route-param-2="create"
             data-route-param-3="{{userid}}"
+            {{#frompanel}}
+                data-route-param-4="frompanel"
+            {{/frompanel}}
         {{/conversationid}}
         data-conversation-id="{{conversationid}}"
         role="button"
index ae5c9a9..3573979 100644 (file)
         data-route="view-conversation"
         {{#conversationid}}
             data-route-param-1="{{.}}"
+            {{#frompanel}}
+                data-route-param-2="frompanel"
+            {{/frompanel}}
         {{/conversationid}}
         {{^conversationid}}
             data-route-param-1="false"
             data-route-param-2="create"
             data-route-param-3="{{id}}"
+            {{#frompanel}}
+                data-route-param-4="frompanel"
+            {{/frompanel}}
         {{/conversationid}}
         role="button"
     >
index 6c816bf..cef660d 100644 (file)
 }}
 <div class="hidden border-bottom px-2 py-3" aria-hidden="true" data-region="view-contacts">
     <div class="d-flex align-items-center">
+        {{#isdrawer}}
         <div class="align-self-stretch">
             <a class="h-100 d-flex align-items-center mr-2" href="#" data-route-back role="button">
                 {{> core_message/message_drawer_icon_back }}
             </a>
         </div>
+        {{/isdrawer}}
         <div>
             {{#str}} contacts, core_message {{/str}}
         </div>
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>
index e28b29a..9f63c26 100644 (file)
 }}
 
 <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 role="button">
-            {{> core_message/message_drawer_icon_back }}
-        </a>
-    </div>
+    {{#showrouteback}}
+        <div class="align-self-stretch" >
+            <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back role="button">
+                {{> core_message/message_drawer_icon_back }}
+            </a>
+        </div>
+    {{/showrouteback}}
     <div class="d-flex text-truncate">
         <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-contact" role="button">
             {{#imageurl}}
index d35d686..7a9a0f2 100644 (file)
 }}
 
 <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 role="button">
-            {{> core_message/message_drawer_icon_back }}
-        </a>
-    </div>
+    {{#showrouteback}}
+        <div class="align-self-stretch" >
+            <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back role="button">
+                {{> core_message/message_drawer_icon_back }}
+            </a>
+        </div>
+    {{/showrouteback}}
     <div class="d-flex text-truncate">
         {{#imageurl}}
             <div class="d-flex align-items-center">
index 7423faa..bda9db7 100644 (file)
 
 <div class="d-flex flex-column">
     <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 role="button">
-                {{> core_message/message_drawer_icon_back }}
-            </a>
-        </div>
+        {{#showrouteback}}
+            <div class="align-self-stretch" >
+                <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back role="button">
+                    {{> core_message/message_drawer_icon_back }}
+                </a>
+            </div>
+        {{/showrouteback}}
         <div class="d-flex text-truncate">
             <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-group-info" role="button">
                 {{#imageurl}}
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 1cbdbda..c5f8d74 100644 (file)
 
 }}
 <div class="d-flex">
+    {{#showrouteback}}
     <div class="align-self-stretch" >
         <a class="h-100 mr-2 d-flex align-items-center" href="#" data-route-back role="button">
             {{> core_message/message_drawer_icon_back }}
         </a>
     </div>
+    {{/showrouteback}}
     <div
         class="ml-2 rounded-circle bg-pulse-grey align-self-center"
         style="height: 38px; width: 38px"
index 9ea7637..edfce42 100644 (file)
@@ -33,7 +33,7 @@
     {}
 
 }}
-<div class="h-100 view-overview-body" aria-hidden="false" data-region="view-overview" data-user-id="{{loggedinuser.id}}">
+<div class="h-100 view-overview-body" aria-hidden="false" data-region="view-overview" {{^isdrawer}}data-in-panel="true"{{/isdrawer}} data-user-id="{{loggedinuser.id}}">
     <div id="message-drawer-view-overview-container-{{uniqid}}" class="d-flex flex-column h-100" style="overflow-y: auto">
             {{> core_message/message_drawer_view_overview_section_favourites }}
             {{> core_message/message_drawer_view_overview_section_group_messages }}
index f0546de..69c30fe 100644 (file)
@@ -33,7 +33,7 @@
     {}
 
 }}
-<div class="border-bottom px-2 py-3" aria-hidden="false" data-region="view-overview">
+<div class="border-bottom px-2 py-3" aria-hidden="false" {{^isdrawer}}data-in-panel="true"{{/isdrawer}} data-region="view-overview">
     <div class="d-flex align-items-center">
         <div class="input-group">
             <div class="input-group-prepend">
index 20bab18..8be4e42 100644 (file)
@@ -42,6 +42,7 @@
     data-users-offset="0"
     data-messages-offset="0"
     style="overflow-y: auto"
+    {{^isdrawer}}data-in-panel="true"{{/isdrawer}}
 >
     <div class="hidden" data-region="search-results-container" style="overflow-y: auto">
         {{> core_message/message_drawer_view_search_results_content }}
index 627bc24..b39202c 100644 (file)
@@ -34,7 +34,7 @@
 
 }}
 
-<div class="hidden border-bottom px-2 py-3 view-search" aria-hidden="true" data-region="view-search">
+<div class="hidden border-bottom px-2 py-3 view-search" {{^isdrawer}}data-in-panel="true"{{/isdrawer}} aria-hidden="true" data-region="view-search">
     <div class="d-flex align-items-center">
         <a
             class="mr-2 align-self-stretch d-flex align-items-center"
index 9ac89fb..a9f717e 100644 (file)
 
 <div class="hidden border-bottom px-2 py-3" aria-hidden="true" data-region="view-settings">
     <div class="d-flex align-items-center">
+        {{#isdrawer}}
         <div class="align-self-stretch" >
             <a class="h-100 d-flex mr-2 align-items-center" href="#" data-route-back role="button">
                 {{> core_message/message_drawer_icon_back }}
             </a>
         </div>
+        {{/isdrawer}}
         <div>
             {{#str}} settings, core {{/str}}
         </div>
index 4c98838..52fc66c 100644 (file)
 }}
 <div
         id="message-index-{{uniqid}}"
-        class="message-app main bg-light border"
+        class="message-app main bg-light border flex-row"
         aria-expanded="false"
         aria-hidden="true"
         data-region="message-index"
         role="region"
 >
-    <div class="header-container position-relative" data-region="header-container">
-        {{> core_message/message_drawer_view_contacts_header }}
-        {{> core_message/message_drawer_view_conversation_header }}
-        {{> core_message/message_drawer_view_overview_header }}
-        {{> core_message/message_drawer_view_search_header }}
-        {{> core_message/message_drawer_view_settings_header }}
-    </div>
-    <div class="body-container position-relative" data-region="body-container">
-        {{> core_message/message_drawer_view_contact_body }}
-        {{> core_message/message_drawer_view_contacts_body }}
-        {{> core_message/message_drawer_view_conversation_body }}
-        {{> core_message/message_drawer_view_group_info_body }}
-        {{> core_message/message_drawer_view_overview_body }}
-        {{> core_message/message_drawer_view_search_body }}
-        {{> core_message/message_drawer_view_settings_body }}
-    </div>
-    <div class="footer-container position-relative" data-region="footer-container">
-        {{> core_message/message_drawer_view_conversation_footer }}
-        {{> core_message/message_drawer_view_overview_footer }}
+    <div class="container-fluid">
+        <div class="row-fluid h-100 no-gutters">
+            <div class="col-4 d-flex flex-column">
+                <div class="border-right h-100">
+                    <div class="panel-header-container" data-region="panel-header-container">
+                        {{> core_message/message_drawer_view_overview_header }}
+                        {{> core_message/message_drawer_view_search_header }}
+                    </div>
+                    <div class="panel-body-container" data-region="panel-body-container">
+                        {{> core_message/message_drawer_view_overview_body }}
+                        {{> core_message/message_drawer_view_search_body }}
+                    </div>
+                </div>
+            </div>
+            <div class="col-8 d-flex flex-column">
+                <div class="header-container position-relative" data-region="header-container">
+                    {{> core_message/message_drawer_view_contacts_header }}
+                    {{> core_message/message_drawer_view_conversation_header }}
+                    {{> core_message/message_drawer_view_settings_header }}
+                </div>
+                <div class="body-container position-relative" data-region="body-container">
+                    {{> core_message/message_drawer_view_contact_body }}
+                    {{> core_message/message_drawer_view_contacts_body }}
+                    {{> core_message/message_drawer_view_conversation_body }}
+                    {{> core_message/message_drawer_view_group_info_body }}
+                    {{> core_message/message_drawer_view_settings_body }}
+                </div>
+                <div class="footer-container position-relative" data-region="footer-container">
+                    {{> core_message/message_drawer_view_conversation_footer }}
+                    {{> core_message/message_drawer_view_overview_footer }}
+                </div>
+            </div>
+        </div>
     </div>
 </div>
 {{#js}}
index 7c7cb7e..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,10 +741,68 @@ 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.
      */
     public function test_search_messages() {
+        $this->resetAfterTest();
+
         // Create some users.
         $user1 = self::getDataGenerator()->create_user();
         $user2 = self::getDataGenerator()->create_user();
@@ -724,13 +811,24 @@ 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,
+            [$user1->id, $user2->id, $user3->id]
+        );
+
         // Send some messages back and forth.
         $time = 1;
-        $this->send_fake_message($user3, $user1, 'Don\'t block me.', 0, $time);
-        $this->send_fake_message($user1, $user2, 'Yo!', 0, $time + 1);
-        $this->send_fake_message($user2, $user1, 'Sup mang?', 0, $time + 2);
-        $this->send_fake_message($user1, $user2, 'Writing PHPUnit tests!', 0, $time + 3);
-        $this->send_fake_message($user2, $user1, 'Word.', 0, $time + 4);
+        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]);
@@ -742,11 +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(3, 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);
@@ -783,6 +882,30 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertTrue($message3->isblocked);
         $this->assertNull($message3->unreadcount);
         $this->assertEquals($conv2id, $message3->conversationid);
+
+        // This is a group conversation. For now, search_messages returns only one of the other users on the conversation. It can't
+        // be guaranteed who will be returned in the first place, so we need to use the in_array to check all the possibilities.
+        $this->assertTrue(in_array($message4->userid, [$user2->id, $user3->id]));
+        $this->assertEquals($user1->id, $message4->useridfrom);
+        $this->assertTrue($message4->ismessaging);
+        $this->assertEquals('My hero!', $message4->lastmessage);
+        $this->assertNotEmpty($message4->messageid);
+        $this->assertNull($message4->isonline);
+        $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);
     }
 
     /**
@@ -1409,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);
     }
 
     /**
@@ -3011,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');
@@ -3047,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);
     }
 
     /**
@@ -3364,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));
@@ -3516,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));
     }
 
     /**
@@ -5754,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.
      */
@@ -5926,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 = [
             [
@@ -5964,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.
             ],
         ];
 
@@ -5980,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' => []
             ],
@@ -5995,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' => []
             ],
@@ -6010,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' => []
             ],
@@ -6025,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' => []
             ],
@@ -6040,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' => []
             ],
@@ -6055,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' => []
             ],
@@ -6070,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' => []
             ],
@@ -6085,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' => []
             ],
@@ -6100,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' => []
             ],
@@ -6115,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' => []
             ],
@@ -6130,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' => []
             ],
@@ -6145,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' => []
             ],
@@ -6160,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' => []
             ],
@@ -6175,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]
             ],
@@ -6190,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]
             ],
@@ -6205,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' => []
             ],
@@ -6220,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' => []
             ],
@@ -6235,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' => []
             ],
@@ -6325,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]);
     }
 
     /**
@@ -6437,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 7609906..fa7ad53 100644 (file)
@@ -367,14 +367,14 @@ class mod_chat_lib_testcase extends advanced_testcase {
         // We don't know when this test is being ran and there is no standard way to
         // mock the time() function (MDL-37327 to handle that).
         if ($hour < 10) {
-            $timezone1 = 'Europe/London';       // GMT or GMT +01:00.
+            $timezone1 = 'UTC';                 // GMT.
             $timezone2 = 'Pacific/Pago_Pago';   // GMT -11:00.
         } else if ($hour < 11) {
             $timezone1 = 'Pacific/Kiritimati';  // GMT +14:00.
             $timezone2 = 'America/Sao_Paulo';   // GMT -03:00.
         } else {
             $timezone1 = 'Pacific/Kiritimati';  // GMT +14:00.
-            $timezone2 = 'Europe/London';       // GMT or GMT +01:00.
+            $timezone2 = 'UTC';                 // GMT.
         }
 
         $this->setTimezone($timezone2);
@@ -411,7 +411,7 @@ class mod_chat_lib_testcase extends advanced_testcase {
         $actionevent22 = mod_chat_core_calendar_provide_event_action($event2, $factory, $student2->id);
 
         // Confirm event1 is not shown to student1 at all.
-        $this->assertNull($actionevent11);
+        $this->assertNull($actionevent11, 'Failed for UTC time ' . gmdate('H:i'));
 
         // Confirm event1 was decorated for student2 and it is actionable.
         $this->assertInstanceOf('\core_calendar\local\event\value_objects\action', $actionevent12);
index 164121a..dc0f375 100644 (file)
@@ -425,6 +425,10 @@ $message-drawer-width: 320px;
     flex-direction: column;
     @include transition();
 
+    .icon-back-in-drawer {
+        display: none;
+    }
+
     &.drawer {
         z-index: $zindex-sticky;
         position: fixed;
@@ -433,16 +437,16 @@ $message-drawer-width: 320px;
         height: calc(100% - #{$navbar-height});
         width: $message-drawer-width;
         box-shadow: -2px 2px 4px rgba(0, 0, 0, .08);
-
-        .body-container {
-            & > * {
-                position: absolute;
-            }
+        .icon-back-in-app {
+            display: none;
+        }
+        .icon-back-in-drawer {
+            display: inherit;
         }
     }
 
     &.main {
-        height: 768px;
+        min-height: 400px;
     }
 
     &.hidden {
@@ -459,6 +463,7 @@ $message-drawer-width: 320px;
         overflow: hidden;
 
         & > * {
+            position: absolute;
             right: 0;
             left: 0;
             top: 0;
@@ -600,7 +605,18 @@ $message-drawer-width: 320px;
         overflow-y: auto;
     }
 }
-
+#page-message-index #region-main {
+    height: 100%;
+    div[role="main"] {
+        height: 100%;
+        #maincontent {
+            margin-top: -1px;
+        }
+        .message-app.main {
+            height: 100%;
+        }
+    }
+}
 .dir-rtl {
     .message-drawer {
         box-shadow: 2px 2px 4px rgba(0, 0, 0, .08);
index 2ea9cba..c3e0e9e 100644 (file)
@@ -13645,6 +13645,8 @@ a.ygtvspacer:hover {
   display: flex;
   flex-direction: column;
   transition: all 0.2s ease-in-out; }
+  .message-app .icon-back-in-drawer {
+    display: none; }
   .message-app.drawer {
     z-index: 1020;
     position: fixed;
@@ -13653,10 +13655,12 @@ a.ygtvspacer:hover {
     height: calc(100% - 50px);
     width: 320px;
     box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.08); }
-    .message-app.drawer .body-container > * {
-      position: absolute; }
+    .message-app.drawer .icon-back-in-app {
+      display: none; }
+    .message-app.drawer .icon-back-in-drawer {
+      display: inherit; }
   .message-app.main {
-    height: 768px; }
+    min-height: 400px; }
   .message-app.hidden {
     display: block;
     right: -320px; }
@@ -13666,6 +13670,7 @@ a.ygtvspacer:hover {
     flex: 1;
     overflow: hidden; }
     .message-app .body-container > * {
+      position: absolute;
       right: 0;
       left: 0;
       top: 0;
@@ -13769,6 +13774,15 @@ a.ygtvspacer:hover {
   .message-app .lazy-load-list {
     overflow-y: auto; }
 
+#page-message-index #region-main {
+  height: 100%; }
+  #page-message-index #region-main div[role="main"] {
+    height: 100%; }
+    #page-message-index #region-main div[role="main"] #maincontent {
+      margin-top: -1px; }
+    #page-message-index #region-main div[role="main"] .message-app.main {
+      height: 100%; }
+
 .dir-rtl .message-drawer {
   box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.08); }
 
index b0abac7..b8852eb 100644 (file)
@@ -13892,6 +13892,8 @@ a.ygtvspacer:hover {
   display: flex;
   flex-direction: column;
   transition: all 0.2s ease-in-out; }
+  .message-app .icon-back-in-drawer {
+    display: none; }
   .message-app.drawer {
     z-index: 1020;
     position: fixed;
@@ -13900,10 +13902,12 @@ a.ygtvspacer:hover {
     height: calc(100% - 50px);
     width: 320px;
     box-shadow: -2px 2px 4px rgba(0, 0, 0, 0.08); }
-    .message-app.drawer .body-container > * {
-      position: absolute; }
+    .message-app.drawer .icon-back-in-app {
+      display: none; }
+    .message-app.drawer .icon-back-in-drawer {
+      display: inherit; }
   .message-app.main {
-    height: 768px; }
+    min-height: 400px; }
   .message-app.hidden {
     display: block;
     right: -320px; }
@@ -13913,6 +13917,7 @@ a.ygtvspacer:hover {
     flex: 1;
     overflow: hidden; }
     .message-app .body-container > * {
+      position: absolute;
       right: 0;
       left: 0;
       top: 0;
@@ -14016,6 +14021,15 @@ a.ygtvspacer:hover {
   .message-app .lazy-load-list {
     overflow-y: auto; }
 
+#page-message-index #region-main {
+  height: 100%; }
+  #page-message-index #region-main div[role="main"] {
+    height: 100%; }
+    #page-message-index #region-main div[role="main"] #maincontent {
+      margin-top: -1px; }
+    #page-message-index #region-main div[role="main"] .message-app.main {
+      height: 100%; }
+
 .dir-rtl .message-drawer {
   box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.08); }
 
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 bad8a94..9371659 100644 (file)
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2019041300.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2019041800.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.
 
-$release  = '3.7dev+ (Build: 20190413)'; // Human-friendly version name
+$release  = '3.7dev+ (Build: 20190418)'; // Human-friendly version name
 
 $branch   = '37';                       // This version's branch.
 $maturity = MATURITY_ALPHA;             // This version's maturity level.