Merge branch 'MDL-64223-master' of https://github.com/snake/moodle
authorJun Pataleta <jun@moodle.com>
Wed, 28 Nov 2018 05:59:50 +0000 (13:59 +0800)
committerJun Pataleta <jun@moodle.com>
Wed, 28 Nov 2018 05:59:50 +0000 (13:59 +0800)
80 files changed:
admin/tool/dataprivacy/db/upgrade.php
admin/tool/dataprivacy/version.php
blocks/myoverview/lang/en/block_myoverview.php
blocks/myoverview/templates/course-action-menu.mustache
blocks/myoverview/templates/view-summary.mustache
blocks/myoverview/tests/behat/block_myoverview_dashboard.feature
course/templates/favouriteicon.mustache
grade/report/singleview/classes/local/screen/screen.php
grade/report/singleview/classes/local/ui/finalgrade.php
grade/report/singleview/tests/behat/bulk_insert_grades.feature
grade/report/singleview/tests/behat/singleview.feature
lang/en/message.php
lib/amd/build/templates.min.js
lib/amd/src/templates.js
lib/classes/message/manager.php
lib/classes/output/icon_system_fontawesome.php
lib/classes/task/delete_unconfirmed_users_task.php
lib/db/services.php
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_overview_section_favourites.min.js [deleted file]
message/amd/build/message_drawer_view_overview_section_group_messages.min.js [deleted file]
message/amd/build/message_drawer_view_overview_section_messages.min.js [deleted file]
message/amd/build/message_repository.min.js
message/amd/src/message_drawer_view_overview.js
message/amd/src/message_drawer_view_overview_section.js
message/amd/src/message_drawer_view_overview_section_favourites.js [deleted file]
message/amd/src/message_drawer_view_overview_section_group_messages.js [deleted file]
message/amd/src/message_drawer_view_overview_section_messages.js [deleted file]
message/amd/src/message_repository.js
message/classes/api.php
message/externallib.php
message/lib.php
message/templates/message_drawer_contacts_list.mustache
message/templates/message_drawer_conversations_list.mustache
message/templates/message_drawer_conversations_list_items_placeholder.mustache [moved from message/templates/message_drawer_conversations_list_item_placeholder.mustache with 60% similarity]
message/templates/message_drawer_lazy_load_list.mustache
message/templates/message_drawer_messages_list.mustache
message/templates/message_drawer_non_contacts_list.mustache
message/templates/message_drawer_view_contacts_body_section_contacts.mustache
message/templates/message_drawer_view_contacts_body_section_requests.mustache
message/templates/message_drawer_view_conversation_body_message.mustache
message/templates/message_drawer_view_conversation_body_messages.mustache
message/templates/message_drawer_view_conversation_footer_edit_mode.mustache
message/templates/message_drawer_view_conversation_header.mustache
message/templates/message_drawer_view_conversation_header_content_type_private.mustache
message/templates/message_drawer_view_conversation_header_content_type_public.mustache
message/templates/message_drawer_view_group_info_participants_list.mustache
message/templates/message_drawer_view_overview_body.mustache
message/templates/message_drawer_view_overview_section.mustache
message/templates/message_drawer_view_overview_section_favourites.mustache
message/templates/message_drawer_view_overview_section_group_messages.mustache
message/templates/message_drawer_view_overview_section_messages.mustache
message/templates/message_drawer_view_search_results_content.mustache
message/tests/api_test.php
message/tests/externallib_test.php
pix/i/checkedcircle.png [new file with mode: 0644]
pix/i/checkedcircle.svg [new file with mode: 0644]
pix/i/uncheckedcircle.png [new file with mode: 0644]
pix/i/uncheckedcircle.svg [new file with mode: 0644]
privacy/classes/local/request/moodle_content_writer.php
theme/boost/scss/moodle/blocks.scss
theme/boost/scss/moodle/core.scss
theme/boost/scss/moodle/grade.scss
theme/boost/scss/moodle/message.scss
theme/boost/style/moodle.css
theme/bootstrapbase/less/bootstrap/sprites.less
theme/bootstrapbase/less/moodle/bs4-compat.less
theme/bootstrapbase/less/moodle/core.less
theme/bootstrapbase/less/moodle/message.less
theme/bootstrapbase/less/moodle/undo.less
theme/bootstrapbase/readme_moodle.txt
theme/bootstrapbase/style/editor.css
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_body_message.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_footer_edit_mode.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_private.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_public.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_overview_section.mustache
version.php

index 2f52d48..99fd156 100644 (file)
@@ -299,5 +299,19 @@ function xmldb_tool_dataprivacy_upgrade($oldversion) {
         upgrade_plugin_savepoint(true, 2018110700, 'tool', 'dataprivacy');
     }
 
+    if ($oldversion < 2018112500) {
+        // Delete orphaned data privacy requests.
+        $sql = "SELECT r.id
+                  FROM {tool_dataprivacy_request} r LEFT JOIN {user} u ON r.userid = u.id
+                 WHERE u.id IS NULL";
+        $orphaned = $DB->get_fieldset_sql($sql);
+
+        if ($orphaned) {
+            $DB->delete_records_list('tool_dataprivacy_request', 'id', $orphaned);
+        }
+
+        upgrade_plugin_savepoint(true, 2018112500, 'tool', 'dataprivacy');
+    }
+
     return true;
 }
index d25e4c8..24cc5a6 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die;
 
-$plugin->version   = 2018110900;
+$plugin->version   = 2018112500;
 $plugin->requires  = 2018050800;        // Moodle 3.5dev (Build 2018031600) and upwards.
 $plugin->component = 'tool_dataprivacy';
index 3bd0631..75c38d3 100644 (file)
@@ -38,7 +38,7 @@ $string['aria:groupingdropdown'] = 'Grouping dropdown';
 $string['aria:inprogress'] = 'Show in courses in progress';
 $string['aria:lastaccessed'] = 'Sort courses by last accessed date';
 $string['aria:list'] = 'Switch to list view';
-$string['aria:title'] = 'Sort courses by title';
+$string['aria:title'] = 'Sort courses by course name';
 $string['aria:past'] = 'Show past courses';
 $string['aria:removefromfavourites'] = 'Remove star for';
 $string['aria:summary'] = 'Switch to summary view';
@@ -63,7 +63,7 @@ $string['privacy:metadata:overviewgroupingpreference'] = 'The Course overview bl
 $string['privacy:metadata:overviewpagingpreference'] = 'The Course overview block paging preference.';
 $string['removefromfavourites'] = 'Unstar this course';
 $string['summary'] = 'Summary';
-$string['title'] = 'Title';
+$string['title'] = 'Course name';
 $string['aria:hidecourse'] = 'Hide {$a} from view';
 $string['aria:showcourse'] = 'Show {$a} in view';
 $string['aria:hiddencourses'] = 'Show hidden courses';
index b1afa75..1ce2d5a 100644 (file)
@@ -41,7 +41,7 @@
             data-course-id="{{id}}"
             aria-controls="favorite-icon-{{ id }}"
             >
-            {{#pix}} i/star, core, {{#str}} favourites, block_myoverview {{/str}} {{/pix}}
+            <span class="text-primary">{{#pix}} i/star, core, {{#str}} favourites, block_myoverview {{/str}} {{/pix}}</span>
             {{#str}} addtofavourites, block_myoverview {{/str}}
             <div class="sr-only">
                 {{#str}} aria:addtofavourites, block_myoverview {{/str}} {{{fullname}}}
index 1921805..abc725f 100644 (file)
@@ -52,7 +52,7 @@
                         {{> core_course/favouriteicon }}
                         <a href="{{viewurl}}" class="coursename">
                             <span class="sr-only">{{#str}}aria:coursename, core_course{{/str}}</span>
-                            <h4 class="d-inline">{{{fullname}}}</h4>
+                            {{{fullname}}}
                         </a>
                     </div>
                     {{> block_myoverview/course-action-menu }}
index 427be51..0e9e623 100644 (file)
@@ -143,12 +143,12 @@ Feature: The my overview block allows users to easily access their courses
     Then I should see "Summary" in the "Course overview" "block"
     And "[data-display='summary']" "css_element" in the "Course overview" "block" should be visible
 
-  Scenario: Title sort persistence
+  Scenario: Course name sort persistence
     Given I log in as "student1"
     And I click on "sortingdropdown" "button" in the "Course overview" "block"
-    And I click on "Title" "link" in the "Course overview" "block"
+    And I click on "Course name" "link" in the "Course overview" "block"
     And I reload the page
-    Then I should see "Title" in the "Course overview" "block"
+    Then I should see "Course name" in the "Course overview" "block"
     And "[data-sort='fullname']" "css_element" in the "Course overview" "block" should be visible
 
   Scenario: Last accessed sort persistence
index 8aa8e6e..31292a8 100644 (file)
@@ -30,7 +30,7 @@
     data-course-id="{{id}}"
     >
     <span
-        class="{{^isfavourite}}hidden{{/isfavourite}}"
+        class="text-primary {{^isfavourite}}hidden{{/isfavourite}}"
         data-region="is-favourite"
         aria-hidden="{{^isfavourite}}true{{/isfavourite}}{{#isfavourite}}false{{/isfavourite}}"
         >
index db3ecb6..891dce8 100644 (file)
@@ -289,6 +289,8 @@ abstract class screen {
         $progressbar->start_html();
         $progressbar->start_progress(get_string('savegrades', 'gradereport_singleview'), count((array) $data) - 1);
         $changecount = array();
+        // This array is used to determine if the override should be excluded from being counted as a change.
+        $ignorevalues = [];
 
         foreach ($data as $varname => $throw) {
             $progressbar->progress($progress);
@@ -351,10 +353,22 @@ abstract class screen {
             }
 
             $msg = $element->set($posted);
+            // Value to check against our list of matchelements to ignore.
+            $check = explode('_', $varname, 2);
 
             // Optional type.
             if (!empty($msg)) {
                 $warnings[] = $msg;
+                if ($element instanceof \gradereport_singleview\local\ui\finalgrade) {
+                    // Add this value to this list so that the override object that is coming next will also be skipped.
+                    $ignorevalues[$check[1]] = $check[1];
+                    // This item wasn't changed so don't add to the changecount.
+                    continue;
+                }
+            }
+            // Check to see if this value has already been skipped.
+            if (array_key_exists($check[1], $ignorevalues)) {
+                continue;
             }
             if (preg_match('/_(\d+)_(\d+)/', $varname, $matchelement)) {
                 $changecount[$matchelement[0]] = 1;
index fe9a17e..fd5e026 100644 (file)
@@ -170,9 +170,11 @@ class finalgrade extends grade_attribute_format implements unique_value, be_disa
             }
             $gradestr->itemname = $this->grade->grade_item->get_name();
             $errorstr = get_string($errorstr, 'grades', $gradestr);
+            return $errorstr;
         }
 
+        // Only update grades if there are no errors.
         $gradeitem->update_final_grade($userid, $finalgrade, 'singleview', $feedback, FORMAT_MOODLE);
-        return $errorstr;
+        return '';
     }
 }
index c99aefe..2f0d4bb 100644 (file)
@@ -101,3 +101,17 @@ Feature: We can bulk insert grades for students in a course
     And the field "Override for Test assignment three" matches value "1"
     And the field "Grade for Test assignment four" matches value "1.00"
     And the field "Override for Test assignment four" matches value "1"
+
+  Scenario: I can not update grades if the value is out of bounds.
+    Given I log in as "teacher1"
+    And I am on "Course 1" course homepage
+    And I navigate to "View > Grader report" in the course gradebook
+    And I follow "Single view for Test assignment one"
+    And I set the field "Perform bulk insert" to "1"
+    When I set the field "Insert value" to "-1"
+    And I press "Save"
+    Then I should see "The grade entered for Test assignment one for james (Student) 1 is less than the minimum allowed"
+    And I should see "The grade entered for Test assignment one for holly (Student) 2 is less than the minimum allowed"
+    And I should see "The grade entered for Test assignment one for anna (Student) 3 is less than the minimum allowed"
+    And I should see "The grade entered for Test assignment one for zac (Student) 4 is less than the minimum allowed"
+    And I should see "Grades were set for 0 items"
index bfd7d7f..d86cdcf 100644 (file)
@@ -116,7 +116,7 @@ Feature: We can use Single view
     And I set the field "Insert value" to "1.0"
     And I set the field "Perform bulk insert" to "1"
     And I press "Save"
-    Then I should see "Grades were set for 8 items"
+    Then I should see "Grades were set for 6 items"
 
   Scenario: Navigation works in the Single view.
     Given I follow "Single view for Student 1"
index 740b929..0a3d9e5 100644 (file)
@@ -46,6 +46,7 @@ $string['contactblocked'] = 'Contact blocked';
 $string['contactrequests'] = 'Contact requests';
 $string['contactrequestsent'] = 'Contact request sent';
 $string['contacts'] = 'Contacts';
+$string['conversationlastmessage'] = '{$a->sender}: {$a->message}';
 $string['decline'] = 'Decline';
 $string['defaultmessageoutputs'] = 'Default message outputs';
 $string['defaults'] = 'Defaults';
@@ -250,6 +251,7 @@ $string['viewunreadmessageswith'] = 'View unread messages with {$a}';
 $string['writeamessage'] = 'Write a message...';
 $string['wouldliketocontactyou'] = 'Would like to contact you';
 $string['you'] = 'You:';
+$string['yousender'] = 'You';
 $string['youhaveblockeduser'] = 'You have blocked this user in the past';
 $string['yourcontactrequestpending'] = 'Your contact request is pending with {$a}';
 
index fedd135..3357ba4 100644 (file)
Binary files a/lib/amd/build/templates.min.js and b/lib/amd/build/templates.min.js differ
index 0c8d929..514f210 100644 (file)
@@ -656,9 +656,10 @@ define([
      * @param {string} templateName - should consist of the component and the name of the template like this:
      *                              core/menu (lib/templates/menu.mustache) or
      *                              tool_bananas/yellow (admin/tool/bananas/templates/yellow.mustache)
+     * @param {Array} parentage - A list of requested partials in this render chain.
      * @return {Promise} JQuery promise object resolved when all partials are in the cache.
      */
-    Renderer.prototype.cachePartials = function(templateName) {
+    Renderer.prototype.cachePartials = function(templateName, parentage) {
         var searchKey = this.currentThemeName + '/' + templateName;
 
         if (searchKey in cachePartialPromises) {
@@ -667,6 +668,9 @@ define([
 
         // This promise will not be resolved until all child partials are also resolved and ready.
         // We create it here to allow us to check for recursive inclusion of templates.
+        // Keep track of the requested partials in this chain.
+        parentage = parentage || [searchKey];
+
         cachePartialPromises[searchKey] = $.Deferred();
 
         this.getTemplate(templateName)
@@ -675,8 +679,8 @@ define([
             var uniquePartials = partials.filter(function(partialName) {
                 // Check for recursion.
 
-                if (typeof cachePartialPromises[this.currentThemeName + '/' + partialName] !== 'undefined') {
-                    // Ignore templates which include their parent.
+                if (parentage.indexOf(this.currentThemeName + '/' + partialName) >= 0) {
+                    // Ignore templates which include a parent template already requested in the current chain.
                     return false;
                 }
 
@@ -686,7 +690,8 @@ define([
 
             // Fetch any partial which has not already been fetched.
             var fetchThemAll = uniquePartials.map(function(partialName) {
-                return this.cachePartials(partialName);
+                parentage.push(this.currentThemeName + '/' + partialName);
+                return this.cachePartials(partialName, parentage);
             }.bind(this));
 
             // Resolve the templateName promise when all of the children are resolved.
index 95bf8c7..774a590 100644 (file)
@@ -82,11 +82,14 @@ class manager {
         $localisedeventdata = clone $eventdata;
 
         // Get user records for all members of the conversation.
+        // We must fetch distinct users, because it's possible for a user to message themselves via bulk user actions.
+        // In such cases, there will be 2 records referring to the same user.
         $sql = "SELECT u.*
-                  FROM {message_conversation_members} mcm
-                  JOIN {user} u
-                    ON (mcm.conversationid = :convid AND u.id = mcm.userid)
-              ORDER BY u.id desc";
+                  FROM {user} u
+                 WHERE u.id IN (
+                          SELECT mcm.userid FROM {message_conversation_members} mcm
+                           WHERE mcm.conversationid = :convid
+                 )";
         $members = $DB->get_records_sql($sql, ['convid' => $eventdata->convid]);
         if (empty($members)) {
             throw new \moodle_exception("Conversation has no members or does not exist.");
index a40689c..1530895 100644 (file)
@@ -200,6 +200,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/calendareventtime' => 'fa-clock-o',
             'core:i/caution' => 'fa-exclamation text-warning',
             'core:i/checked' => 'fa-check',
+            'core:i/checkedcircle' => 'fa-check-circle',
             'core:i/checkpermissions' => 'fa-unlock-alt',
             'core:i/cohort' => 'fa-users',
             'core:i/competencies' => 'fa-check-square-o',
@@ -306,6 +307,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/trash' => 'fa-trash',
             'core:i/twoway' => 'fa-arrows-h',
             'core:i/unchecked' => 'fa-square-o',
+            'core:i/uncheckedcircle' => 'fa-circle-o',
             'core:i/unflagged' => 'fa-flag-o',
             'core:i/unlock' => 'fa-unlock',
             'core:i/up' => 'fa-arrow-up',
index b9aa9a3..40dfe8e 100644 (file)
@@ -54,9 +54,8 @@ class delete_unconfirmed_users_task extends scheduled_task {
                                             WHERE confirmed = 0 AND timecreated > 0
                                                   AND timecreated < ? AND deleted = 0", array($cuttime));
             foreach ($rs as $user) {
-                delete_user($user); // We MUST delete user properly first.
-                $DB->delete_records('user', array('id' => $user->id)); // This is a bloody hack, but it might work.
-                mtrace(" Deleted unconfirmed user for ".fullname($user, true)." ($user->id)");
+                delete_user($user);
+                mtrace(" Deleted unconfirmed user ".fullname($user, true)." ($user->id)");
             }
             $rs->close();
         }
index f4bd168..6d930b8 100644 (file)
@@ -1143,6 +1143,15 @@ $functions = array(
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
         'ajax' => true,
     ),
+    'core_message_get_unread_conversation_counts' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'get_unread_conversation_counts',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Retrieve a list of unread conversation counts, indexed by type.',
+        'type' => 'read',
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+        'ajax' => true,
+    ),
     'core_message_get_conversation_members' => array(
         'classname' => 'core_message_external',
         'methodname' => 'get_conversation_members',
index d050e94..acaa285 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 f1efaf7..f96085c 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
diff --git a/message/amd/build/message_drawer_view_overview_section_favourites.min.js b/message/amd/build/message_drawer_view_overview_section_favourites.min.js
deleted file mode 100644 (file)
index fce097f..0000000
Binary files a/message/amd/build/message_drawer_view_overview_section_favourites.min.js and /dev/null differ
diff --git a/message/amd/build/message_drawer_view_overview_section_group_messages.min.js b/message/amd/build/message_drawer_view_overview_section_group_messages.min.js
deleted file mode 100644 (file)
index 0c3bb2b..0000000
Binary files a/message/amd/build/message_drawer_view_overview_section_group_messages.min.js and /dev/null differ
diff --git a/message/amd/build/message_drawer_view_overview_section_messages.min.js b/message/amd/build/message_drawer_view_overview_section_messages.min.js
deleted file mode 100644 (file)
index 4b2c2ba..0000000
Binary files a/message/amd/build/message_drawer_view_overview_section_messages.min.js and /dev/null differ
index 08e84a3..ae786be 100644 (file)
Binary files a/message/amd/build/message_repository.min.js and b/message/amd/build/message_repository.min.js differ
index c68486f..e214fca 100644 (file)
@@ -26,24 +26,22 @@ define(
     'core/key_codes',
     'core/pubsub',
     'core/str',
-    'core_message/message_drawer_view_overview_section_favourites',
-    'core_message/message_drawer_view_overview_section_group_messages',
-    'core_message/message_drawer_view_overview_section_messages',
     'core_message/message_drawer_router',
     'core_message/message_drawer_routes',
-    'core_message/message_drawer_events'
+    'core_message/message_drawer_events',
+    'core_message/message_drawer_view_overview_section',
+    'core_message/message_repository'
 ],
 function(
     $,
     KeyCodes,
     PubSub,
     Str,
-    Favourites,
-    GroupMessages,
-    Messages,
     Router,
     Routes,
-    MessageDrawerEvents
+    MessageDrawerEvents,
+    Section,
+    MessageRepository
 ) {
 
     var SELECTORS = {
@@ -51,7 +49,97 @@ function(
         FAVOURITES: '[data-region="view-overview-favourites"]',
         GROUP_MESSAGES: '[data-region="view-overview-group-messages"]',
         MESSAGES: '[data-region="view-overview-messages"]',
-        SEARCH_INPUT: '[data-region="view-overview-search-input"]'
+        SEARCH_INPUT: '[data-region="view-overview-search-input"]',
+        SECTION_TOGGLE_BUTTON: '[data-toggle]'
+    };
+
+    var CONVERSATION_TYPES = {
+        PRIVATE: 1,
+        PUBLIC: 2,
+        FAVOURITE: null
+    };
+
+    var loadAllCountsPromise = null;
+
+    /**
+     * Load the total and unread conversation counts from the server for this user. This function
+     * returns a jQuery promise that will be resolved with the counts.
+     *
+     * The request is only sent once per page load and will be cached for subsequent
+     * calls to this function.
+     *
+     * @param {Number} loggedInUserId The logged in user's id
+     * @return {Object} jQuery promise
+     */
+    var loadAllCounts = function(loggedInUserId) {
+        if (loadAllCountsPromise === null) {
+            loadAllCountsPromise = MessageRepository.getAllConversationCounts(loggedInUserId);
+        }
+
+        return loadAllCountsPromise;
+    };
+
+    /**
+     * Filter a set of counts to return only the count for the given type.
+     *
+     * 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).
+     * @return {Number}
+     */
+    var filterCountsByType = function(counts, type) {
+        return type === CONVERSATION_TYPES.FAVOURITE ? counts.favourites : counts.types[type];
+    };
+
+    /**
+     * Opens one of the sections based on whether the section has unread conversations
+     * or any conversations
+     *
+     * Default section priority is favourites, groups, then messages. A section can increase
+     * in priority if it has conversations in it. It can increase even further if it has
+     * unread conversations.
+     *
+     * @param {Array} sections List of section roots, total counts, and unread counts.
+     */
+    var openSection = function(sections) {
+        var isAlreadyOpen = sections.some(function(section) {
+            var sectionRoot = section[0];
+            return Section.isVisible(sectionRoot);
+        });
+
+        if (isAlreadyOpen) {
+            // The user has already opened a section so there is nothing to do.
+            return;
+        }
+
+        // Order the sections so that sections with unread conversations are prioritised
+        // over sections without and sections with total conversations are prioritised
+        // over sections without.
+        sections.sort(function(a, b) {
+            var aTotal = a[1];
+            var aUnread = a[2];
+            var bTotal = b[1];
+            var bUnread = b[2];
+
+            if (aUnread > 0 && bUnread == 0) {
+                return -1;
+            } else if (aUnread == 0 && bUnread > 0) {
+                return 1;
+            } else if (aTotal > 0 && bTotal == 0) {
+                return -1;
+            } else if (aTotal == 0 && bTotal > 0) {
+                return 1;
+            } else {
+                return 0;
+            }
+        });
+
+        // Get the root of the first section after sorting.
+        var sectionRoot = sections[0][0];
+        var button = sectionRoot.find(SELECTORS.SECTION_TOGGLE_BUTTON);
+        // Click it to expand it.
+        button.click();
     };
 
     /**
@@ -64,6 +152,16 @@ function(
         return header.find(SELECTORS.SEARCH_INPUT);
     };
 
+    /**
+     * Get the logged in user id.
+     *
+     * @param {Object} body Overview body container element.
+     * @return {String} Logged in user id.
+     */
+    var getLoggedInUserId = function(body) {
+        return body.attr('data-user-id');
+    };
+
     /**
      * Decrement the contact request count. If the count is zero or below then
      * hide the count.
@@ -121,12 +219,45 @@ function(
         }
 
         getSearchInput(header).val('');
+        var loggedInUserId = getLoggedInUserId(body);
+        var allCounts = loadAllCounts(loggedInUserId);
+
+        var sections = [
+            // Favourite conversations section.
+            [body.find(SELECTORS.FAVOURITES), CONVERSATION_TYPES.FAVOURITE, true],
+            // Group conversations section.
+            [body.find(SELECTORS.GROUP_MESSAGES), CONVERSATION_TYPES.PUBLIC, false],
+            // Private conversations section.
+            [body.find(SELECTORS.MESSAGES), CONVERSATION_TYPES.PRIVATE, false]
+        ];
+
+        sections.forEach(function(args) {
+            var sectionRoot = args[0];
+            var sectionType = args[1];
+            var includeFavourites = args[2];
+            var totalCountPromise = allCounts.then(function(result) {
+                return filterCountsByType(result.total, sectionType);
+            });
+            var unreadCountPromise = allCounts.then(function(result) {
+                return filterCountsByType(result.unread, sectionType);
+            });
+
+            Section.show(sectionRoot, sectionType, includeFavourites, totalCountPromise, unreadCountPromise);
+        });
+
+        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);
+
+                    return [sectionRoot, totalCount, unreadCount];
+                });
 
-        return $.when(
-            Favourites.show(body.find(SELECTORS.FAVOURITES)),
-            GroupMessages.show(body.find(SELECTORS.GROUP_MESSAGES)),
-            Messages.show(body.find(SELECTORS.MESSAGES))
-        );
+                // Open up one of the sections for the user.
+                return openSection(sectionParams);
+            });
     };
 
     /**
index 8a88ba8..5489219 100644 (file)
@@ -60,15 +60,20 @@ function(
         LAST_MESSAGE_DATE: '[data-region="last-message-date"]',
         UNREAD_COUNT: '[data-region="unread-count"]',
         SECTION_TOTAL_COUNT: '[data-region="section-total-count"]',
-        SECTION_UNREAD_COUNT: '[data-region="section-unread-count"]'
+        SECTION_TOTAL_COUNT_CONTAINER: '[data-region="section-total-count-container"]',
+        SECTION_UNREAD_COUNT: '[data-region="section-unread-count"]',
+        PLACEHOLDER_CONTAINER: '[data-region="placeholder-container"]'
     };
 
     var TEMPLATES = {
-        CONVERSATIONS_LIST: 'core_message/message_drawer_conversations_list'
+        CONVERSATIONS_LIST: 'core_message/message_drawer_conversations_list',
+        CONVERSATIONS_LIST_ITEMS_PLACEHOLDER: 'core_message/message_drawer_conversations_list_items_placeholder'
     };
 
     var LOAD_LIMIT = 50;
     var loadedConversationsById = {};
+    var loadedTotalCounts = false;
+    var loadedUnreadCounts = false;
 
     /**
      * Get the section visibility status.
@@ -98,6 +103,53 @@ function(
         root.removeClass('expanded');
     };
 
+    /**
+     * Render the total count value and show it for the user. Also update the placeholder
+     * HTML for better visuals.
+     *
+     * @param {Object} root The section container element.
+     * @param {Number} count The total count
+     */
+    var renderTotalCount = function(root, count) {
+        var container = root.find(SELECTORS.SECTION_TOTAL_COUNT_CONTAINER);
+        var countElement = container.find(SELECTORS.SECTION_TOTAL_COUNT);
+        countElement.text(count);
+        container.removeClass('hidden');
+
+        var numPlaceholders = count > 20 ? 20 : count;
+        // Array of "true" up to the number of placeholders we want.
+        var placeholders = Array.apply(null, Array(numPlaceholders)).map(function() {
+            return true;
+        });
+
+        // Replace the current placeholder (loading spinner) with some nicer placeholders that
+        // better represent the content.
+        Templates.render(TEMPLATES.CONVERSATIONS_LIST_ITEMS_PLACEHOLDER, {placeholders: placeholders})
+            .then(function(html) {
+                var placeholderContainer = root.find(SELECTORS.PLACEHOLDER_CONTAINER);
+                placeholderContainer.html(html);
+                return;
+            })
+            .catch(function() {
+                // Silently ignore. Doesn't matter if we can't render the placeholders.
+            });
+    };
+
+    /**
+     * Render the unread count value and show it for the user if it's higher than zero.
+     *
+     * @param {Object} root The section container element.
+     * @param {Number} count The unread count
+     */
+    var renderUnreadCount = function(root, count) {
+        var countElement = root.find(SELECTORS.SECTION_UNREAD_COUNT);
+        countElement.text(count);
+
+        if (count > 0) {
+            countElement.removeClass('hidden');
+        }
+    };
+
     /**
      * Render the messages in the overview page.
      *
@@ -108,7 +160,9 @@ function(
      */
     var render = function(contentContainer, conversations, userId) {
         var formattedConversations = conversations.map(function(conversation) {
+
             var lastMessage = conversation.messages.length ? conversation.messages[conversation.messages.length - 1] : null;
+
             var formattedConversation = {
                 id: conversation.id,
                 imageurl: conversation.imageurl,
@@ -134,6 +188,15 @@ function(
                 formattedConversation.isblocked = otherUser.isblocked;
             }
 
+            if (conversation.type == MessageDrawerViewConversationContants.CONVERSATION_TYPES.PUBLIC) {
+                formattedConversation.lastsendername = conversation.members.reduce(function(carry, member) {
+                    if (!carry && member.id == lastMessage.useridfrom) {
+                        carry = member.fullname;
+                    }
+                    return carry;
+                }, null);
+            }
+
             return formattedConversation;
         });
 
@@ -209,10 +272,12 @@ function(
      * @param  {Object} root Overview messages container element.
      */
     var incrementTotalConversationCount = function(root) {
-        var element = getTotalConversationCountElement(root);
-        var count = parseInt(element.text());
-        count = count + 1;
-        element.text(count);
+        if (loadedTotalCounts) {
+            var element = getTotalConversationCountElement(root);
+            var count = parseInt(element.text());
+            count = count + 1;
+            element.text(count);
+        }
     };
 
     /**
@@ -221,10 +286,12 @@ function(
      * @param  {Object} root Overview messages container element.
      */
     var decrementTotalConversationCount = function(root) {
-        var element = getTotalConversationCountElement(root);
-        var count = parseInt(element.text());
-        count = count - 1;
-        element.text(count);
+        if (loadedTotalCounts) {
+            var element = getTotalConversationCountElement(root);
+            var count = parseInt(element.text());
+            count = count - 1;
+            element.text(count);
+        }
     };
 
     /**
@@ -233,13 +300,15 @@ function(
      * @param  {Object} root Overview messages container element.
      */
     var decrementTotalUnreadConversationCount = function(root) {
-        var element = getTotalUnreadConversationCountElement(root);
-        var count = parseInt(element.text());
-        count = count - 1;
-        element.text(count);
-
-        if (count < 1) {
-            element.addClass('hidden');
+        if (loadedUnreadCounts) {
+            var element = getTotalUnreadConversationCountElement(root);
+            var count = parseInt(element.text());
+            count = count - 1;
+            element.text(count);
+
+            if (count < 1) {
+                element.addClass('hidden');
+            }
         }
     };
 
@@ -294,7 +363,7 @@ function(
         var message = conversation.messages[conversation.messages.length - 1];
         var youString = '';
         var stringRequests = [
-            {key: 'you', component: 'core_message'},
+            {key: 'yousender', component: 'core_message'},
             {key: 'strftimetime24', component: 'core_langconfig'},
         ];
         return Str.get_strings(stringRequests)
@@ -306,15 +375,16 @@ function(
                 return dates[0];
             })
             .then(function(dateString) {
-                var lastMessage = $(message.text).text();
-
-                if (message.fromLoggedInUser) {
-                    lastMessage = youString + ' ' + lastMessage;
-                }
-
-                element.find(SELECTORS.LAST_MESSAGE).html(lastMessage);
                 element.find(SELECTORS.LAST_MESSAGE_DATE).text(dateString).removeClass('hidden');
-                return dateString;
+
+                // Now load the last message.
+                return Str.get_string('conversationlastmessage', 'core_message', {
+                    sender: message.fromLoggedInUser ? youString : message.userFrom.fullname,
+                    message: "<span class='text-muted'>" + $(message.text).text() + "</span>"
+                });
+            })
+            .then(function(lastMessage) {
+                return element.find(SELECTORS.LAST_MESSAGE).html(lastMessage);
             });
     };
 
@@ -519,8 +589,10 @@ function(
      * @param {Object} root The section container element.
      * @param {Number} type The conversation type for 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
      */
-    var show = function(root, type, includeFavourites) {
+    var show = function(root, type, includeFavourites, totalCountPromise, unreadCountPromise) {
         root = $(root);
 
         if (!root.attr('data-init')) {
@@ -533,11 +605,33 @@ function(
                 LazyLoadList.show(listRoot, loadCallback, render);
             }
 
+            // This is given to us by the calling code because the total counts for all sections
+            // are loaded in a single ajax request rather than one request per section.
+            totalCountPromise.then(function(count) {
+                renderTotalCount(root, count);
+                loadedTotalCounts = true;
+                return;
+            })
+            .catch(function() {
+                // Silently ignore if we can't updated the counts. No need to bother the user.
+            });
+
+            // Same as for total counts.
+            unreadCountPromise.then(function(count) {
+                renderUnreadCount(root, count);
+                loadedTotalCounts = true;
+                return;
+            })
+            .catch(function() {
+                // Silently ignore if we can't updated the counts. No need to bother the user.
+            });
+
             root.attr('data-init', true);
         }
     };
 
     return {
-        show: show
+        show: show,
+        isVisible: isVisible
     };
 });
diff --git a/message/amd/src/message_drawer_view_overview_section_favourites.js b/message/amd/src/message_drawer_view_overview_section_favourites.js
deleted file mode 100644 (file)
index 1634010..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-// 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/>.
-
-/**
- * Controls the favourites section of the overview page in the message drawer.
- *
- * @module     core_message/message_drawer_view_overview_section_favourites
- * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define(
-[
-    'jquery',
-    'core_message/message_drawer_view_overview_section'
-],
-function(
-    $,
-    Section
-) {
-    // All conversation types.
-    var CONVERSATION_TYPE = null;
-    var INCLUDE_FAVOURITES = true;
-
-    /**
-     * Show the overview page conversations.
-     *
-     * @param {Object} root Overview messages container element.
-     */
-    var show = function(root) {
-        Section.show($(root), CONVERSATION_TYPE, INCLUDE_FAVOURITES);
-    };
-
-    return {
-        show: show,
-    };
-});
diff --git a/message/amd/src/message_drawer_view_overview_section_group_messages.js b/message/amd/src/message_drawer_view_overview_section_group_messages.js
deleted file mode 100644 (file)
index 4713d97..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-// 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/>.
-
-/**
- * Controls the group overview section of the overview page in the message drawer.
- *
- * @module     core_message/message_drawer_view_overview_section_group_messages
- * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define(
-[
-    'jquery',
-    'core_message/message_drawer_view_overview_section'
-],
-function(
-    $,
-    Section
-) {
-    // Public conversations.
-    var CONVERSATION_TYPE = 2;
-    var INCLUDE_FAVOURITES = false;
-
-    /**
-     * Show the overview page conversations.
-     *
-     * @param {Object} root Overview messages container element.
-     */
-    var show = function(root) {
-        root = $(root);
-        Section.show($(root), CONVERSATION_TYPE, INCLUDE_FAVOURITES);
-    };
-
-    return {
-        show: show,
-    };
-});
diff --git a/message/amd/src/message_drawer_view_overview_section_messages.js b/message/amd/src/message_drawer_view_overview_section_messages.js
deleted file mode 100644 (file)
index 75be370..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-// 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/>.
-
-/**
- * Controls the messages section of the overview page in the message drawer.
- *
- * @module     core_message/message_drawer_view_overview_section_messages
- * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
- * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-define(
-[
-    'jquery',
-    'core_message/message_drawer_view_overview_section'
-],
-function(
-    $,
-    Section
-) {
-    // Private conversations.
-    var CONVERSATION_TYPE = 1;
-    var INCLUDE_FAVOURITES = false;
-
-    /**
-     * Show the overview page conversations.
-     *
-     * @param {Object} root Overview messages container element.
-     */
-    var show = function(root) {
-        Section.show($(root), CONVERSATION_TYPE, INCLUDE_FAVOURITES);
-    };
-
-    return {
-        show: show,
-    };
-});
index 7b20649..809b7a0 100644 (file)
@@ -970,6 +970,67 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
         return Ajax.call([request])[0];
     };
 
+    /**
+     * The the count of the user's conversations grouped by type.
+     *
+     * @param {Number} userId The user's id.
+     * @return {Object} jQuery promise.
+     */
+    var getTotalConversationCounts = function(userId) {
+        var request = {
+            methodname: 'core_message_get_conversation_counts',
+            args: {
+                userid: userId
+            }
+        };
+        return Ajax.call([request])[0];
+    };
+
+    /**
+     * The the count of the user's unread conversations grouped by type.
+     *
+     * @param {Number} userId The user's id.
+     * @return {Object} jQuery promise.
+     */
+    var getUnreadConversationCounts = function(userId) {
+        var request = {
+            methodname: 'core_message_get_unread_conversation_counts',
+            args: {
+                userid: userId
+            }
+        };
+        return Ajax.call([request])[0];
+    };
+
+    /**
+     * Get both the unread and total conversation counts in a single request.
+     *
+     * @param {Number} userId The user's id.
+     * @return {Object} jQuery promise.
+     */
+    var getAllConversationCounts = function(userId) {
+        var requests = [
+            {
+                methodname: 'core_message_get_conversation_counts',
+                args: {
+                    userid: userId
+                }
+            },
+            {
+                methodname: 'core_message_get_unread_conversation_counts',
+                args: {
+                    userid: userId
+                }
+            },
+        ];
+        return $.when.apply(null, Ajax.call(requests)).then(function(total, unread) {
+            return {
+                total: total,
+                unread: unread
+            };
+        });
+    };
+
     return {
         query: query,
         countUnreadConversations: countUnreadConversations,
@@ -1002,6 +1063,9 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
         unsetFavouriteConversations: unsetFavouriteConversations,
         getMemberInfo: getMemberInfo,
         markAllConversationMessagesAsRead: markAllConversationMessagesAsRead,
-        getUserMessagePreferences: getUserMessagePreferences
+        getUserMessagePreferences: getUserMessagePreferences,
+        getTotalConversationCounts: getTotalConversationCounts,
+        getUnreadConversationCounts: getUnreadConversationCounts,
+        getAllConversationCounts: getAllConversationCounts
     };
 });
index d579223..c6d280d 100644 (file)
@@ -586,6 +586,7 @@ class api {
         $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).
         $members = [];
         $individualmembers = [];
         $groupmembers = [];
@@ -613,6 +614,9 @@ 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.
@@ -654,6 +658,23 @@ 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:
@@ -702,7 +723,7 @@ class api {
         // MEMBER COUNT.
         $cids = array_column($conversations, 'id');
         list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid');
-        $membercountsql = "SELECT conversationid, count(id) AS membercount
+        $membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount
                              FROM {message_conversation_members} mcm
                             WHERE mcm.conversationid $cidinsql
                          GROUP BY mcm.conversationid";
@@ -738,6 +759,11 @@ class api {
                 continue;
             }
 
+            // Exclude 'self' conversations for now.
+            if (isset($selfconversations[$conversation->id])) {
+                continue;
+            }
+
             $conv = new \stdClass();
             $conv->id = $conversation->id;
             $conv->name = $conversation->conversationname;
@@ -839,6 +865,12 @@ class api {
             return null;
         }
 
+        // Get the context of the conversation. This will be used to check whether the conversation is a favourite.
+        // This will be either 'user' (for individual conversations) or, in the case of linked conversations,
+        // the context stored in the record.
+        $userctx = \context_user::instance($userid);
+        $conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid);
+
         $isconversationmember = $DB->record_exists(
             'message_conversation_members',
             [
@@ -873,7 +905,7 @@ class api {
         );
 
         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
-        $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $systemcontext);
+        $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);
 
         $convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
         $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
@@ -1497,6 +1529,7 @@ 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.
         // - 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
@@ -1512,6 +1545,17 @@ class api {
                   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
@@ -1527,7 +1571,10 @@ class api {
                $favsql
                  WHERE mcm.userid = :userid3
                    AND mc.enabled = :enabled
-                   AND ((mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL) OR (mc.type = :grouptype))
+                   AND (
+                          (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL AND membercount > 1) OR
+                          (mc.type = :grouptype)
+                       )
               GROUP BY mc.type, fav.itemtype
               ORDER BY mc.type ASC";
 
@@ -1536,6 +1583,7 @@ class api {
             'userid2' => $userid,
             'userid3' => $userid,
             'userid4' => $userid,
+            'userid5' => $userid,
             'action' => self::MESSAGE_ACTION_DELETED,
             'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
             'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
@@ -2511,7 +2559,14 @@ class api {
      */
     public static function get_received_contact_requests_count(int $userid) : int {
         global $DB;
-        return $DB->count_records('message_contact_requests', ['requesteduserid' => $userid]);
+        $sql = "SELECT COUNT(mcr.id)
+                  FROM {message_contact_requests} mcr
+             LEFT JOIN {message_users_blocked} mub
+                    ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
+                 WHERE mcr.requesteduserid = :requesteduserid
+                   AND mub.id IS NULL";
+        $params = ['requesteduserid' => $userid];
+        return $DB->count_records_sql($sql, $params);
     }
 
     /**
index 76fffba..b98ddd5 100644 (file)
@@ -4480,4 +4480,79 @@ class core_message_external extends external_api {
             ]
         );
     }
+
+    /**
+     * Returns description of method parameters for get_unread_conversation_counts() method.
+     *
+     * @return external_function_parameters
+     */
+    public static function get_unread_conversation_counts_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'id of the user, 0 for current user', VALUE_DEFAULT, 0)
+            ]
+        );
+    }
+
+    /**
+     * Returns an array of unread conversation counts for the various types of conversations, including favourites.
+     *
+     * Return format:
+     * [
+     *     'favourites' => 0,
+     *     'types' => [
+     *          \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+     *          \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+     *      ]
+     * ]
+     *
+     * @param int $userid the id of the user whose counts we are fetching.
+     * @return array the array of unread conversation counts, indexed by type.
+     * @throws moodle_exception if the current user cannot perform this action.
+     */
+    public static function get_unread_conversation_counts(int $userid) {
+        global $CFG, $USER;
+
+        // All the business logic checks that really shouldn't be in here.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        if (empty($userid)) {
+            $userid = $USER->id;
+        }
+
+        $params = ['userid' => $userid];
+        $params = self::validate_parameters(self::get_unread_conversation_counts_parameters(), $params);
+
+        $systemcontext = context_system::instance();
+        self::validate_context($systemcontext);
+
+        if (($USER->id != $params['userid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+            throw new moodle_exception('You do not have permission to perform this action.');
+        }
+
+        return \core_message\api::get_unread_conversation_counts($params['userid']);
+    }
+
+    /**
+     * Get unread conversation counts return description.
+     *
+     * @return external_description
+     */
+    public static function get_unread_conversation_counts_returns() {
+        return new external_single_structure(
+            [
+                'favourites' => new external_value(PARAM_INT, 'Total number of unread favourite conversations'),
+                'types' => new external_single_structure(
+                    [
+                        \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => new external_value(PARAM_INT,
+                            'Total number of unread individual conversations'),
+                        \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => new external_value(PARAM_INT,
+                            'Total number of unread group conversations'),
+                    ]
+                ),
+            ]
+        );
+    }
 }
index c734c5c..2d46e4d 100644 (file)
@@ -834,12 +834,6 @@ function core_message_standard_after_main_region_html() {
     }
 
     $renderer = $PAGE->get_renderer('core');
-
-    // Get the counts.
-    $conversationcounts = \core_message\api::get_conversation_counts($USER->id);
-    $individualconversationcount = $conversationcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL];
-    $groupconversationcount = $conversationcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP];
-    $favouriteconversationcount = $conversationcounts['favourites'];
     $requestcount = \core_message\api::get_received_contact_requests_count($USER->id);
     $contactscount = \core_message\api::count_contacts($USER->id);
 
@@ -866,54 +860,12 @@ function core_message_standard_after_main_region_html() {
     // Get the unread counts for the current user.
     $unreadcounts = \core_message\api::get_unread_conversation_counts($USER->id);
 
-    // Determine which section will be expanded.
-    // Default behaviour - if no unread counts exist.
-    $favouritesexpanded = !empty($favouriteconversationcount);
-    $groupmessagesexpanded = empty($favouriteconversationcount) && !empty($groupconversationcount);
-    $messagesexpanded = empty($favouriteconversationcount) && empty($groupconversationcount);
-
-    // There is an unread conversation somewhere, so that takes priority.
-    if ($unreadcounts['favourites'] > 0 || $unreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP] > 0 ||
-            $unreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL] > 0) {
-        $favouritesexpanded = $unreadcounts['favourites'] > 0;
-        $groupmessagesexpanded = !$favouritesexpanded &&
-            $unreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP] > 0;
-        $messagesexpanded = !$groupmessagesexpanded && !$favouritesexpanded &&
-            $unreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL] > 0;
-    }
-
     return $renderer->render_from_template('core_message/message_drawer', [
         'contactrequestcount' => $requestcount,
         'loggedinuser' => [
             'id' => $USER->id,
             'midnight' => usergetmidnight(time())
         ],
-        'overview' => [
-            'messages' => [
-                'expanded' => $messagesexpanded,
-                'count' => [
-                    'unread' => $unreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL],
-                    'total' => $individualconversationcount
-                ],
-                'placeholders' => array_fill(0, $individualconversationcount, true)
-            ],
-            'groupmessages' => [
-                'expanded' => $groupmessagesexpanded,
-                'count' => [
-                    'unread' => $unreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP],
-                    'total' => $groupconversationcount
-                ],
-                'placeholders' => array_fill(0, $groupconversationcount, true)
-            ],
-            'favourites' => [
-                'expanded' => $favouritesexpanded,
-                'count' => [
-                    'unread' => $unreadcounts['favourites'],
-                    'total' => $favouriteconversationcount
-                ],
-                'placeholders' => array_fill(0, $favouriteconversationcount, true)
-            ],
-        ],
         'contacts' => [
             'sectioncontacts' => [
                 'placeholders' => array_fill(0, $contactscount > 50 ? 50 : $contactscount, true)
index 8f9b3aa..37f7fc9 100644 (file)
@@ -37,7 +37,7 @@
 {{#contacts}}
     <a
         href="#"
-        class="p-2 d-flex list-group-item list-group-item-action border-0"
+        class="p-2 d-flex list-group-item list-group-item-action"
         data-route="view-conversation"
         {{#conversationid}}
             data-route-param-1="{{.}}"
index 3ffe642..5cdee0e 100644 (file)
@@ -37,7 +37,7 @@
 {{#conversations}}
     <a
         href="#"
-        class="p-2 d-flex list-group-item list-group-item-action border-0 align-items-center"
+        class="py-0 px-2 d-flex list-group-item list-group-item-action align-items-center"
         data-conversation-id="{{id}}"
         {{#userid}}
             data-user-id="{{.}}"
@@ -45,7 +45,7 @@
     >
         {{#imageurl}}
             <img
-                class="rounded-circle"
+                class="rounded-circle align-self-start mt-2"
                 src="{{{.}}}"
                 alt="{{name}}"
                 aria-hidden="true"
         {{#showonlinestatus}}
             <span class="contact-status {{#isonline}}online{{/isonline}}"></span>
         {{/showonlinestatus}}
-        <div class="w-100 text-truncate ml-2">
+        <div class="w-100 text-truncate ml-2 my-2">
             <div class="d-flex">
                 <strong class="m-0 text-truncate">{{name}}</strong>
                 <span class="{{^isblocked}}hidden{{/isblocked}}" data-region="contact-icon-blocked">
                     {{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}}
                 </span>
-                <div
-                    class="ml-auto small {{^lastmessagedate}}hidden{{/lastmessagedate}}"
-                    data-region="last-message-date"
-                >
-                    {{#lastmessagedate}}
-                        {{#userdate}} {{.}}, {{#str}} strftimetime24, core_langconfig  {{/str}} {{/userdate}}
-                    {{/lastmessagedate}}
-                </div>
             </div>
             {{#subname}}
                 <p class="m-0 text-truncate">{{.}}</p>
             {{/subname}}
-            <p class="m-0 font-weight-light text-truncate" data-region="last-message">
-                {{#sentfromcurrentuser}}{{#str}} you, core_message {{/str}}{{/sentfromcurrentuser}}
-                {{lastmessage}}
+            <p class="m-0 font-weight-light text-truncate last-message" data-region="last-message">
+                {{#sentfromcurrentuser}}
+                    {{#str}}
+                        conversationlastmessage,
+                        core_message,
+                        {
+                            "sender": "{{#str}} yousender, core_message {{/str}}",
+                            "message": "<span class='text-muted'>{{lastmessage}}</span>"
+                        }
+                    {{/str}}
+                {{/sentfromcurrentuser}}
+                {{^sentfromcurrentuser}}
+                    {{#lastsendername}}
+                        {{#str}}
+                            conversationlastmessage,
+                            core_message,
+                            {
+                                "sender": "{{.}}",
+                                "message": "<span class='text-muted'>{{lastmessage}}</span>"
+                            }
+                        {{/str}}
+                    {{/lastsendername}}
+                    {{^lastsendername}}
+                        <span class='text-muted'>{{lastmessage}}</span>
+                    {{/lastsendername}}
+                {{/sentfromcurrentuser}}
             </p>
         </div>
+        <div class="d-flex align-self-stretch">
+            <div
+                class="px-2 py-1 small position-absolute position-right text-muted {{^lastmessagedate}}hidden{{/lastmessagedate}}"
+                data-region="last-message-date"
+            >
+                {{#lastmessagedate}}
+                    {{#userdate}} {{.}}, {{#str}} strftimetime24, core_langconfig  {{/str}} {{/userdate}}
+                {{/lastmessagedate}}
+            </div>
+            <div class="d-flex align-self-center align-items-center">
+                <span
+                    class="badge badge-pill badge-primary bg-primary {{^unreadcount}}hidden{{/unreadcount}}"
+                    data-region="unread-count"
+                >
+                    {{unreadcount}}
+                </span>
 
-        <span
-            class="badge badge-pill badge-primary bg-primary rounded-circle {{^unreadcount}}hidden{{/unreadcount}}"
-            data-region="unread-count"
-        >
-            {{unreadcount}}
-        </span>
-
-        <div class="text-muted">
-            {{> core_message/message_drawer_icon_forward }}
+                <div class="text-muted ml-auto">
+                    {{> core_message/message_drawer_icon_forward }}
+                </div>
+            </div>
         </div>
     </a>
 {{/conversations}}
@@ -15,7 +15,7 @@
     along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 }}
 {{!
-    @template core_message/message_drawer_conversations_list_item_placeholder
+    @template core_message/message_drawer_conversations_list_items_placeholder
 
     This template will render a placeholder loading element for a message in
     the message drawer.
     {}
 
 }}
-<div class="p-2 d-flex list-group-item border-0" data-region="placeholder">
-    <div
-        class="rounded-circle bg-pulse-grey"
-        style="height: 38px; width: 38px"
-    >
-    </div>
-    <div class="ml-2 align-self-center" style="flex: 1">
+{{#placeholders}}
+    <div class="p-2 d-flex list-group-item border-0" data-region="placeholder">
         <div
-            class="bg-pulse-grey w-50"
-            style="height: 15px;"
+            class="rounded-circle bg-pulse-grey"
+            style="height: 38px; width: 38px"
         >
         </div>
+        <div class="ml-2 align-self-center" style="flex: 1">
+            <div
+                class="bg-pulse-grey w-50"
+                style="height: 15px;"
+            >
+            </div>
+            <div
+                class="bg-pulse-grey mt-1"
+                style="height: 15px; width: 90%"
+            >
+            </div>
+        </div>
         <div
-            class="bg-pulse-grey mt-1"
-            style="height: 15px; width: 90%"
+            class="bg-pulse-grey align-self-center"
+            style="height: 15px; width: 15px"
+        >
+        </div>
+        <div
+            class="ml-2 bg-pulse-grey align-self-center"
+            style="height: 15px; width: 15px"
         >
         </div>
     </div>
-    <div
-        class="bg-pulse-grey align-self-center"
-        style="height: 15px; width: 15px"
-    >
-    </div>
-    <div
-        class="ml-2 bg-pulse-grey align-self-center"
-        style="height: 15px; width: 15px"
-    >
-    </div>
-</div>
\ No newline at end of file
+{{/placeholders}}
\ No newline at end of file
index 4079ed1..2dc8690 100644 (file)
@@ -34,8 +34,7 @@
 
 }}
 <div
-    class="{{$rootclasses}}{{/rootclasses}}"
-    style="overflow-y: auto"
+    class="{{$rootclasses}}{{/rootclasses}} lazy-load-list"
     aria-live="polite"
     data-region="lazy-load-list"
     data-user-id="{{loggedinuser.id}}"
index 68c4953..3373a79 100644 (file)
@@ -37,7 +37,7 @@
 {{#messages}}
     <a
         href="#"
-        class="p-2 d-flex list-group-item list-group-item-action border-0 align-items-center"
+        class="p-2 d-flex list-group-item list-group-item-action align-items-center"
         data-route="view-conversation"
         {{#conversationid}}
             data-route-param-1="{{.}}"
@@ -84,7 +84,7 @@
         </div>
 
         <span
-            class="badge badge-pill badge-primary bg-primary rounded-circle {{^unreadcount}}hidden{{/unreadcount}}"
+            class="badge badge-pill badge-primary bg-primary {{^unreadcount}}hidden{{/unreadcount}}"
             data-region="unread-count"
         >
             {{unreadcount}}
index f3d8ae2..19852d9 100644 (file)
@@ -37,7 +37,7 @@
 {{#noncontacts}}
     <a
         href="#"
-        class="p-2 d-flex list-group-item list-group-item-action border-0"
+        class="p-2 d-flex list-group-item list-group-item-action"
         data-route="view-conversation"
         {{#conversationid}}
             data-route-param-1="{{.}}"
index 484b096..f37a202 100644 (file)
     {{$emptymessage}}{{#str}} nocontactsgetstarted, core_message {{/str}}{{/emptymessage}}
     {{$placeholder}}
         {{#contacts}}
-            {{#sectionrequests}}
+            {{#sectioncontacts}}
                 {{#placeholders}}
                     {{> core_message/message_drawer_contacts_list_item_placeholder }}
                 {{/placeholders}}
-            {{/sectionrequests}}
+            {{/sectioncontacts}}
         {{/contacts}}
     {{/placeholder}}
 {{/ core_message/message_drawer_lazy_load_list }}
index 120bf21..4791b7c 100644 (file)
     {{$emptymessage}}{{#str}} nocontactrequests, core_message {{/str}}{{/emptymessage}}
     {{$placeholder}}
         {{#contacts}}
-                {{#sectioncontacts}}
-                    {{#placeholders}}
-                        {{> core_message/message_drawer_contacts_list_item_placeholder }}
-                    {{/placeholders}}
-            {{/sectioncontacts}}
+            {{#sectionrequests}}
+                {{#placeholders}}
+                    {{> core_message/message_drawer_contacts_list_item_placeholder }}
+                {{/placeholders}}
+            {{/sectionrequests}}
         {{/contacts}}
     {{/placeholder}}
 {{/ core_message/message_drawer_lazy_load_list }}
index c6ef486..5bfca2d 100644 (file)
@@ -35,7 +35,7 @@
 
 }}
 <div
-    class="message clickable d-flex flex-column p-2 {{#fromloggedinuser}}bg-secondary{{/fromloggedinuser}}{{^fromloggedinuser}}bg-white{{/fromloggedinuser}} rounded mb-2"
+    class="message clickable d-flex flex-column p-2 {{#fromloggedinuser}}bg-secondary{{/fromloggedinuser}}{{^fromloggedinuser}}bg-white{{/fromloggedinuser}} rounded mb-2 mt-2"
     data-region="message"
     data-message-id="{{id}}"
     role="checkbox"
@@ -44,7 +44,7 @@
 >
     <div class="d-flex align-items-center {{^fromloggedinuser}}pb-2{{/fromloggedinuser}}">
         {{^fromloggedinuser}}
-        <div>
+        <div style="flex-shrink: 0">
             <img
                 class="rounded-circle"
                 src="{{{userfrom.profileimageurl}}}"
@@ -53,8 +53,8 @@
                 style="height: 30px"
             >
         </div>
-        <div class="pl-2 pr-2">
-            <h6 class="text-truncate m-0">{{userfrom.fullname}}</h6>
+        <div class="text-truncate pl-2 pr-2">
+            <h6 class="text-truncate m-0 font-weight-bold">{{userfrom.fullname}}</h6>
         </div>
         {{/fromloggedinuser}}
         <div
@@ -63,8 +63,8 @@
         >
             {{formattedtime}}
         </div>
-        <span class="hidden ml-2 small text-primary" data-region="not-selected-icon">{{#pix}} i/unchecked, core {{/pix}}</span>
-        <span class="hidden ml-2 small text-primary" data-region="selected-icon">{{#pix}} i/checked, core {{/pix}}</span>
+        <span class="hidden ml-2 small text-dark" data-region="not-selected-icon">{{#pix}} i/uncheckedcircle, core {{/pix}}</span>
+        <span class="hidden ml-2 small text-primary" data-region="selected-icon">{{#pix}} i/checkedcircle, core {{/pix}}</span>
     </div>
     <div>{{{text}}}</div>
 </div>
\ No newline at end of file
index b87ecd8..7d234cb 100644 (file)
@@ -34,7 +34,7 @@
     {}
 
 }}
-<div data-region="day-messages-container">
+<div class="d-flex flex-column" data-region="day-messages-container">
     {{#messages}}
         {{> core_message/message_drawer_view_conversation_body_message }}
     {{/messages}}
index c1f2bde..f1c9127 100644 (file)
@@ -35,7 +35,7 @@
 
 }}
 
-<div class="d-flex p-3">
+<div class="d-flex p-3 justify-content-end">
     <button
         class="btn btn-link btn-icon my-1 icon-size-4"
         data-action="delete-selected-messages"
index 64d2039..66f84bf 100644 (file)
@@ -35,7 +35,7 @@
 }}
 
 <div
-    class="hidden bg-white position-relative border-bottom px-2 py-3"
+    class="hidden bg-white position-relative border-bottom px-2 py-2"
     aria-hidden="true"
     data-region="view-conversation"
 >
index 5d614f9..df7b4db 100644 (file)
@@ -42,7 +42,7 @@
         </a>
     </div>
     <div class="d-flex text-truncate">
-        <a class="d-flex text-truncate" href="#" data-action="view-contact">
+        <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-contact">
             {{#imageurl}}
                 <div class="d-flex align-items-center">
                     <img
index 07950f8..71a68fa 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>
-            {{> core_message/message_drawer_icon_back }}
-        </a>
-    </div>
-    <div class="d-flex text-truncate">
-        <a class="d-flex text-truncate" href="#" data-action="view-group-info">
-            {{#imageurl}}
-                <div class="d-flex align-items-center">
+<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>
+                {{> core_message/message_drawer_icon_back }}
+            </a>
+        </div>
+        <div class="d-flex text-truncate">
+            <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-group-info">
+                {{#imageurl}}
                     <img
                         class="rounded-circle"
                         src="{{{.}}}"
                         aria-hidden="true"
                         style="height: 38px"
                     >
+                {{/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>
+                    <p class="m-0 text-truncate">{{subname}}</p>
                 </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>
-                <p class="m-0 text-truncate">{{subname}}</p>
-                <p class="m-0 font-weight-light text-truncate">
-                    {{#str}} numparticipants, core_message, {{totalmembercount}} {{/str}}
-                </p>
-            </div>
-        </a>
-    </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" href="#" data-action="view-group-info">
-                {{#str}} groupinfo, core_message {{/str}}
-            </a>
-            <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>
         </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" href="#" data-action="view-group-info">
+                    {{#str}} groupinfo, core_message {{/str}}
+                </a>
+                <a class="dropdown-item {{#isfavourite}}hidden{{/isfavourite}}" href="#" data-action="confirm-favourite">
+                    {{#str}} addtofavourites, core_message {{/str}}
+                </a>
+                <a class="dropdown-item {{^isfavourite}}hidden{{/isfavourite}}" href="#" data-action="confirm-unfavourite">
+                    {{#str}} removefromfavourites, core_message {{/str}}
+                </a>
+            </div>
+        </div>
+    </div>
+    <div class="d-flex">
+        <!-- These are just placeholder elements to align the text below correctly with the name text above -->
+        <div class="mr-2 icon" aria-hidden="true"></div>
+        {{#imageurl}}<div style="width: 38px" aria-hidden="true"></div>{{/imageurl}}
+        <!-- End placeholders -->
+        <a class="text-decoration-none line-height-3 ml-2" href="#" data-action="view-group-info">
+            <small class="m-0 text-muted text-truncate">
+                {{#str}} numparticipants, core_message, {{totalmembercount}} {{/str}}
+            </small>
+        </a>
     </div>
 </div>
\ No newline at end of file
index c863e35..fc7d839 100644 (file)
@@ -37,7 +37,7 @@
 {{#contacts}}
     <a
         href="#"
-        class="p-2 d-flex list-group-item list-group-item-action border-0"
+        class="p-2 d-flex list-group-item list-group-item-action"
         data-route="view-conversation"
         {{#conversationid}}
             data-route-param-1="{{.}}"
index 6c93a13..8ba2939 100644 (file)
     {}
 
 }}
-<div class="h-100 view-overview-body" aria-hidden="false" data-region="view-overview">
+<div class="h-100 view-overview-body" aria-hidden="false" data-region="view-overview" data-user-id="{{loggedinuser.id}}">
     <div id="message-drawer-view-overview-container" class="d-flex flex-column h-100" style="overflow-y: auto">
-        {{#overview.favourites}}
             {{> core_message/message_drawer_view_overview_section_favourites }}
-        {{/overview.favourites}}
-        {{#overview.groupmessages}}
             {{> core_message/message_drawer_view_overview_section_group_messages }}
-        {{/overview.groupmessages}}
-        {{#overview.messages}}
             {{> core_message/message_drawer_view_overview_section_messages }}
-        {{/overview.messages}}
     </div>
 </div>
index 2b1e960..b0e1135 100644 (file)
             <span class="expanded-icon-container">
                 {{#pix}} t/expanded, core {{/pix}}
             </span>
-            {{$title}}{{/title}} (<span data-region="section-total-count">{{count.total}}</span>)
+            <span class="font-weight-bold">{{$title}}{{/title}}</span>
+            <span class="hidden ml-1" data-region="section-total-count-container">
+                (<span data-region="section-total-count">{{count.total}}</span>)
+            </span>
             <span class="hidden ml-2" data-region="loading-icon-container">
                 {{> core/loading }}
             </span>
@@ -70,4 +73,4 @@
             data-parent="#message-drawer-view-overview-container"
         {{/rootattributes}}
     {{/ core_message/message_drawer_lazy_load_list }}
-</div>
\ No newline at end of file
+</div>
index e0bf51a..b2987e4 100644 (file)
@@ -39,9 +39,7 @@
     {{$region}}view-overview-favourites{{/region}}
     {{$title}}{{#str}} favourites {{/str}}{{/title}}
     {{$placeholder}}
-        {{#placeholders}}
-            {{> core_message/message_drawer_conversations_list_item_placeholder }}
-        {{/placeholders}}
+        <div class="text-center py-2">{{> core/loading }}</div>
     {{/placeholder}}
     {{$emptymessage}}
         <p class="text-muted mt-2">{{#str}} nofavourites, core_message {{/str}}</p>
index 25b5cad..165c5f5 100644 (file)
@@ -39,9 +39,7 @@
     {{$region}}view-overview-group-messages{{/region}}
     {{$title}}{{#str}} groupmessages, core_message {{/str}}{{/title}}
     {{$placeholder}}
-        {{#placeholders}}
-            {{> core_message/message_drawer_conversations_list_item_placeholder }}
-        {{/placeholders}}
+        <div class="text-center py-2">{{> core/loading }}</div>
     {{/placeholder}}
     {{$emptymessage}}
         <p class="text-muted mt-2">{{#str}} nogroupmessages, core_message {{/str}}</p>
index dddfc11..baf5d3b 100644 (file)
@@ -39,9 +39,7 @@
     {{$region}}view-overview-messages{{/region}}
     {{$title}}{{#str}} messages, core_message {{/str}}{{/title}}
     {{$placeholder}}
-        {{#placeholders}}
-            {{> core_message/message_drawer_conversations_list_item_placeholder }}
-        {{/placeholders}}
+        <div class="text-center py-2">{{> core/loading }}</div>
     {{/placeholder}}
     {{$emptymessage}}
         <p class="text-muted mt-2">{{#str}} nomessages, core_message {{/str}}</p>
index 624ee12..7ffc183 100644 (file)
 }}
 
 <div class="d-flex flex-column">
-    <div class="mb-3 bg-white p-2">
-        <div data-region="contacts-container">
-            <h4 class="h6">{{#str}} contacts, core_message {{/str}}</h4>
+    <div class="mb-3 bg-white">
+        <div data-region="contacts-container"  class="pt-2">
+            <h4 class="h6 px-2">{{#str}} contacts, core_message {{/str}}</h4>
             <div class="list-group" data-region="list"></div>
-            <p class="hidden" data-region="no-results-container">
+            <p class="hidden px-2" data-region="no-results-container">
                 {{#str}} searchnocontactsfound, core_message {{/str}}
             </p>
         </div>
-        <div data-region="non-contacts-container">
-            <h4 class="h6">{{#str}} noncontacts, core_message {{/str}}</h4>
+        <div data-region="non-contacts-container" class="pt-2 border-top">
+            <h4 class="h6 px-2">{{#str}} noncontacts, core_message {{/str}}</h4>
             <div class="list-group" data-region="list"></div>
-            <p class="hidden" data-region="no-results-container">
+            <p class="hidden px-2" data-region="no-results-container">
                 {{#str}} searchnononcontactsfound, core_message {{/str}}
             </p>
         </div>
             </button>
         </div>
     </div>
-    <div class="bg-white p-2">
+    <div class="bg-white">
         <div data-region="messages-container">
-            <h4 class="h6">{{#str}} messages, core_message {{/str}}</h4>
+            <h4 class="h6 px-2 pt-2">{{#str}} messages, core_message {{/str}}</h4>
             <div class="list-group" data-region="list"></div>
-            <p class="hidden" data-region="no-results-container">
+            <p class="hidden px-2" data-region="no-results-container">
                 {{#str}} searchnomessagesfound, core_message {{/str}}
             </p>
         </div>
index 6f8d266..1465a55 100644 (file)
@@ -1364,6 +1364,31 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $conversations = \core_message\api::get_conversations($user1->id, 0, 20, 0);
     }
 
+    /**
+     * Tests retrieving conversations when a legacy 'self' conversation exists.
+     */
+    public function test_get_conversations_legacy_self_conversations() {
+        global $DB;
+
+        // Create a legacy 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!');
+
+        // 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);
+        $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);
+    }
+
     /**
      * Tests retrieving conversations when a conversation contains a deleted user.
      */
@@ -4729,6 +4754,24 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertObjectHasAttribute('iscontact', $request);
     }
 
+    /**
+     * Test the get_contact_requests() function when the user has blocked the sender of the request.
+     */
+    public function test_get_contact_requests_blocked_sender() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // User1 blocks User2.
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        // User2 tries to add User1 as a contact.
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+
+        // Verify we don't see the contact request from the blocked user User2 in the requests for User1.
+        $requests = \core_message\api::get_contact_requests($user1->id);
+        $this->assertEmpty($requests);
+    }
+
     /**
      * Test getting contact requests when there are none.
      */
@@ -4918,6 +4961,23 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(2, \core_message\api::get_received_contact_requests_count($user1->id));
     }
 
+    /**
+     * Test the get_received_contact_requests_count() function when the user has blocked the sender of the request.
+     */
+    public function test_get_received_contact_requests_count_blocked_sender() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // User1 blocks User2.
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        // User2 tries to add User1 as a contact.
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+
+        // Verify we don't see the contact request from the blocked user User2 in the count for User1.
+        $this->assertEquals(0, \core_message\api::get_received_contact_requests_count($user1->id));
+    }
+
     /**
      * Test the get_contact_requests_between_users() function.
      */
@@ -5681,19 +5741,19 @@ 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;
-        list($user1, $user2, $user3, $user4, $user5, $user6, $user7) = [0, 1, 2, 3, 4, 5, 6];
+        list($user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8) = [0, 1, 2, 3, 4, 5, 6, 7];
         $conversations = [
             [
                 'type' => $typeindividual,
                 'users' => [$user1, $user2],
-                'messages' => [$user1, $user2],
+                'messages' => [$user1, $user2, $user2],
                 'favourites' => [$user1],
                 'enabled' => null // Individual conversations cannot be disabled.
             ],
             [
                 'type' => $typeindividual,
                 'users' => [$user1, $user3],
-                'messages' => [$user1, $user1],
+                'messages' => [$user1, $user3, $user1],
                 'favourites' => [],
                 'enabled' => null // Individual conversations cannot be disabled.
             ],
@@ -5704,13 +5764,27 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'favourites' => [],
                 'enabled' => true
             ],
+            [
+                'type' => $typegroup,
+                'users' => [$user2, $user3, $user4],
+                'messages' => [$user2, $user3, $user4],
+                'favourites' => [],
+                'enabled' => true
+            ],
             [
                 'type' => $typegroup,
                 'users' => [$user6, $user7],
-                'messages' => [$user6, $user7],
+                'messages' => [$user6, $user7, $user7],
                 'favourites' => [$user6],
                 'enabled' => false
             ],
+            [
+                'type' => $typeindividual,
+                'users' => [$user8, $user8],
+                'messages' => [$user8, $user8],
+                'favourites' => [],
+                'enabled' => null // Individual conversations cannot be disabled.
+            ],
         ];
 
         return [
@@ -5719,20 +5793,28 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user5],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
                 ]],
                 'deletedusers' => []
             ],
-            'No individual conversations, 1 group conversation' => [
+            'No individual conversations, 2 group conversations' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user4],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
@@ -5741,31 +5823,43 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
                 'deletedusers' => []
             ],
-            '1 individual conversation, 1 group conversation' => [
+            '1 individual conversation, 2 group conversations' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user2],
-                'expected' => ['favourites' => 0, 'types' => [
+                '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 => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
-            '1 group conversation only' => [
+            '2 group conversations only' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user4],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
@@ -5774,7 +5868,11 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'deletemessagesuser' => $user1,
                 'deletemessages' => [0],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -5783,9 +5881,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             'All conversation types, delete a message from individual non-favourited, messages remaining' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [2],
+                'deletemessages' => [3],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -5794,9 +5896,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             'All conversation types, delete all messages from individual favourited, no messages remaining' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [0, 1],
+                'deletemessages' => [0, 1, 2],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -5805,9 +5911,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             'All conversation types, delete all messages from individual non-favourited, no messages remaining' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [2, 3],
+                'deletemessages' => [3, 4, 5],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -5816,31 +5926,43 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             'All conversation types, delete all messages from individual favourited, no messages remaining, different user' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [0, 1],
+                'deletemessages' => [0, 1, 2],
                 'arguments' => [$user2],
-                'expected' => ['favourites' => 0, 'types' => [
+                '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 => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
             'All conversation types, delete all messages from individual non-favourited, no messages remaining, different user' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [2, 3],
+                'deletemessages' => [3, 4, 5],
                 'arguments' => [$user3],
-                'expected' => ['favourites' => 0, 'types' => [
+                '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 => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
             'All conversation types, delete some messages from group non-favourited, messages remaining,' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [4, 5],
+                'deletemessages' => [6, 7],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -5849,12 +5971,16 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             'All conversation types, delete all messages from group non-favourited, no messages remaining,' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [4, 5, 6, 7],
+                'deletemessages' => [6, 7, 8, 9],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
                 'deletedusers' => []
             ],
             'All conversation types, another user soft deleted' => [
@@ -5862,7 +5988,11 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -5873,7 +6003,11 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -5884,7 +6018,11 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user6],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
                 ]],
@@ -5895,7 +6033,26 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user7],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'deletedusers' => []
+            ],
+            'Conversation with self' => [
+                'conversationConfigs' => $conversations,
+                'deletemessagesuser' => null,
+                'deletemessages' => [],
+                'arguments' => [$user8],
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
                 ]],
@@ -5912,7 +6069,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
      * @param int $deletemessagesuser The user who is deleting the messages
      * @param array $deletemessages The list of messages to delete (by index)
      * @param array $arguments Arguments for the count conversations function
-     * @param array $expected The expected result
+     * @param array $expectedcounts the expected conversation counts
+     * @param array $expectedunreadcounts the expected unread conversation counts
      * @param array $deletedusers the array of users to soft delete.
      */
     public function test_get_conversation_counts(
@@ -5920,11 +6078,13 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $deletemessagesuser,
         $deletemessages,
         $arguments,
-        $expected,
+        $expectedcounts,
+        $expectedunreadcounts,
         $deletedusers
     ) {
         $generator = $this->getDataGenerator();
         $users = [
+            $generator->create_user(),
             $generator->create_user(),
             $generator->create_user(),
             $generator->create_user(),
@@ -5975,10 +6135,10 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         $counts = \core_message\api::get_conversation_counts(...$arguments);
 
-        $this->assertEquals($expected['favourites'], $counts['favourites']);
-        $this->assertEquals($expected['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL],
+        $this->assertEquals($expectedcounts['favourites'], $counts['favourites']);
+        $this->assertEquals($expectedcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL],
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
-        $this->assertEquals($expected['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP],
+        $this->assertEquals($expectedcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP],
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
     }
 
@@ -6012,90 +6172,86 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
     }
 
     /**
-     * Test verifying the correctness of unread counts returned.
-     */
-    public function test_get_unread_conversations_count() {
-        // Get a bunch of conversations, some group, some individual and in different states.
-        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
-            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
-
-        // Without favourites.
-        $counts = \core_message\api::get_unread_conversation_counts($user1->id);
-        $this->assertEquals(0, $counts['favourites']);
-        $this->assertEquals(2, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
-        $this->assertEquals(2, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
-
-        // Mark a couple as favourites.
-        \core_message\api::set_favourite_conversation($ic1->id, $user1->id);
-        \core_message\api::set_favourite_conversation($gc2->id, $user1->id);
-
-        $counts = \core_message\api::get_unread_conversation_counts($user1->id);
-        $this->assertEquals(2, $counts['favourites']);
-        $this->assertEquals(1, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
-        $this->assertEquals(1, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
-
-        // Mark a conversation as read and confirm it's not included in the unread counts for its respective type.
-        \core_message\api::mark_all_messages_as_read($user1->id, $ic1->id);
-        $counts = \core_message\api::get_unread_conversation_counts($user1->id);
-        $this->assertEquals(1, $counts['favourites']);
-        $this->assertEquals(1, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
-        $this->assertEquals(1, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
-    }
-
-    /**
-     * Test verifying the unread counts are 0 if no conversations exist.
-     */
-    public function test_get_unread_conversations_count_no_conversations() {
-        $user1 = self::getDataGenerator()->create_user();
-        $counts = \core_message\api::get_unread_conversation_counts($user1->id);
-        $this->assertEquals(0, $counts['favourites']);
-        $this->assertEquals(0, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
-        $this->assertEquals(0, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
-    }
-
-    /**
-     * Test verifying that those linked conversations which have been disabled are excluded from unread counts.
+     * Test the get_unread_conversation_counts() function.
+     *
+     * @dataProvider test_get_conversation_counts_test_cases()
+     * @param array $conversationconfigs Conversations to create
+     * @param int $deletemessagesuser The user who is deleting the messages
+     * @param array $deletemessages The list of messages to delete (by index)
+     * @param array $arguments Arguments for the count conversations function
+     * @param array $expectedcounts the expected conversation counts
+     * @param array $expectedunreadcounts the expected unread conversation counts
+     * @param array $deletedusers the list of users to soft-delete.
      */
-    public function test_get_unread_conversations_count_disabled_conversations() {
-        global $DB;
+    public function test_get_unread_conversation_counts(
+        $conversationconfigs,
+        $deletemessagesuser,
+        $deletemessages,
+        $arguments,
+        $expectedcounts,
+        $expectedunreadcounts,
+        $deletedusers
+    ) {
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+        $users = [
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user()
+        ];
 
-        // Create some users.
-        $user1 = self::getDataGenerator()->create_user();
-        $user2 = self::getDataGenerator()->create_user();
-        $user3 = self::getDataGenerator()->create_user();
+        $deleteuser = !is_null($deletemessagesuser) ? $users[$deletemessagesuser] : null;
+        $this->setUser($users[$arguments[0]]);
+        $arguments[0] = $users[$arguments[0]]->id;
+        $systemcontext = \context_system::instance();
+        $conversations = [];
+        $messageids = [];
 
-        $course1 = $this->getDataGenerator()->create_course();
+        foreach ($conversationconfigs as $config) {
+            $conversation = \core_message\api::create_conversation(
+                $config['type'],
+                array_map(function($userindex) use ($users) {
+                    return $users[$userindex]->id;
+                }, $config['users']),
+                null,
+                ($config['enabled'] ?? true)
+            );
 
-        // Create a group with a linked conversation and a valid image.
-        $this->setAdminUser();
-        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
-        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
-        $this->getDataGenerator()->enrol_user($user3->id, $course1->id);
-        $group1 = $this->getDataGenerator()->create_group([
-            'courseid' => $course1->id,
-            'enablemessaging' => 1,
-        ]);
+            foreach ($config['messages'] as $userfromindex) {
+                $userfrom = $users[$userfromindex];
+                $messageids[] = testhelper::send_fake_message_to_conversation($userfrom, $conversation->id);
+            }
 
-        // Add users to group1.
-        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id));
-        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id));
+            foreach ($config['favourites'] as $userfromindex) {
+                $userfrom = $users[$userfromindex];
+                $usercontext = \context_user::instance($userfrom->id);
+                $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
+                $ufservice->create_favourite('core_message', 'message_conversations', $conversation->id, $systemcontext);
+            }
 
-        $conversations = \core_message\api::get_conversations($user1->id);
-        $convid = $conversations[0]->id;
+            $conversations[] = $conversation;
+        }
 
-        // Send a message to the group conversation as user 2.
-        testhelper::send_fake_message_to_conversation($user2, $convid, 'Hello world!');
+        foreach ($deletemessages as $messageindex) {
+            \core_message\api::delete_message($deleteuser->id, $messageids[$messageindex]);
+        }
 
-        // Verify the unread count is 1.
-        $counts = \core_message\api::get_unread_conversation_counts($user1->id);
-        $this->assertEquals(1, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
+        foreach ($deletedusers as $deleteduser) {
+            delete_user($users[$deleteduser]);
+        }
 
-        // Now, disabled the linked conversation.
-        $DB->set_field('message_conversations', 'enabled', false, ['id' => $convid]);
+        $counts = \core_message\api::get_unread_conversation_counts(...$arguments);
 
-        // Verify the unread count is no longer 1 as the conversation has been excluded.
-        $counts = \core_message\api::get_unread_conversation_counts($user1->id);
-        $this->assertEquals(0, $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
+        $this->assertEquals($expectedunreadcounts['favourites'], $counts['favourites']);
+        $this->assertEquals($expectedunreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL],
+            $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]);
     }
 
     /**
index 2d7dea8..b4be382 100644 (file)
@@ -587,6 +587,28 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertArrayHasKey('iscontact', $request);
     }
 
+    /**
+     * Test the get_contact_requests() function when the user has blocked the sender of the request.
+     */
+    public function test_get_contact_requests_blocked_sender() {
+        $this->resetAfterTest();
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // User1 blocks User2.
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        // User2 tries to add User1 as a contact.
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+
+        // Verify we don't see the contact request from the blocked user User2 in the requests for User1.
+        $this->setUser($user1);
+        $requests = core_message_external::get_contact_requests($user1->id);
+        $requests = external_api::clean_returnvalue(core_message_external::get_contact_requests_returns(), $requests);
+
+        $this->assertCount(0, $requests);
+    }
+
     /**
      * Test getting contact requests when there are none.
      */
@@ -704,6 +726,28 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(2, $contactrequestnumber);
     }
 
+    /**
+     * Test the get_received_contact_requests_count() function when the user has blocked the sender of the request.
+     */
+    public function test_get_received_contact_requests_count_blocked_sender() {
+        $this->resetAfterTest();
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // User1 blocks User2.
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        // User2 tries to add User1 as a contact.
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+
+        // Verify we don't see the contact request from the blocked user User2 in the count for User1.
+        $this->setUser($user1);
+        $contactrequestnumber = core_message_external::get_received_contact_requests_count($user1->id);
+        $contactrequestnumber = external_api::clean_returnvalue(
+            core_message_external::get_received_contact_requests_count_returns(), $contactrequestnumber);
+        $this->assertEquals(0, $contactrequestnumber);
+    }
+
     /**
      * Test getting the number of received contact requests with no permissions.
      */
@@ -5289,6 +5333,35 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         core_message_external::get_conversations($user1->id, 0, 20, 0);
     }
 
+    /**
+     * Tests retrieving conversations when a legacy 'self' conversation exists.
+     */
+    public function test_get_conversations_legacy_self_conversations() {
+        global $DB;
+        $this->resetAfterTest();
+
+        // Create a legacy 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!');
+
+        // 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);
+        $member = array_pop($members);
+        $this->assertEquals($user1->id, $member->userid);
+
+        // Verify this conversation is not 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);
+    }
+
     /**
      * Tests retrieving conversations when a conversation contains a deleted user.
      */
@@ -5949,6 +6022,89 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv);
     }
 
+    /**
+     * Test verifying that the correct favourite information is returned for a non-linked converastion at user context.
+     */
+    public function test_get_conversation_favourited() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // Create a conversation between the 2 users.
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id,
+            ],
+            'An individual conversation'
+        );
+
+        // Favourite the conversation as user 1 only.
+        \core_message\api::set_favourite_conversation($conversation->id, $user1->id);
+
+        // Get the conversation for user1 and confirm it's favourited.
+        $this->setUser($user1);
+        $conv = core_message_external::get_conversation($user1->id, $conversation->id);
+        $conv = external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv);
+        $this->assertTrue($conv['isfavourite']);
+
+        // Get the conversation for user2 and confirm it's NOT favourited.
+        $this->setUser($user2);
+        $conv = core_message_external::get_conversation($user2->id, $conversation->id);
+        $conv = external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv);
+        $this->assertFalse($conv['isfavourite']);
+    }
+
+    /**
+     * Test verifying that the correct favourite information is returned for a group-linked conversation at course context.
+     */
+    public function test_get_conversation_favourited_group_linked() {
+        $this->resetAfterTest();
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+        $course1context = \context_course::instance($course1->id);
+
+        // Create a group with a linked conversation and a valid image.
+        $this->setAdminUser();
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course1->id);
+        $group1 = $this->getDataGenerator()->create_group([
+            'courseid' => $course1->id,
+            'enablemessaging' => 1
+        ]);
+
+        // Add users to group1.
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id));
+
+        // Verify that the conversation is a group linked conversation in the course context.
+        $conversationrecord = $DB->get_record('message_conversations', ['component' => 'core_group', 'itemtype' => 'groups']);
+        $this->assertEquals($course1context->id, $conversationrecord->contextid);
+
+        // Favourite the conversation as user 1 only.
+        \core_message\api::set_favourite_conversation($conversationrecord->id, $user1->id);
+
+        // Get the conversation for user1 and confirm it's favourited.
+        $this->setUser($user1);
+        $conv = core_message_external::get_conversation($user1->id, $conversationrecord->id);
+        $conv = external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv);
+        $this->assertTrue($conv['isfavourite']);
+
+        // Get the conversation for user2 and confirm it's NOT favourited.
+        $this->setUser($user2);
+        $conv = core_message_external::get_conversation($user2->id, $conversationrecord->id);
+        $conv = external_api::clean_returnvalue(core_message_external::get_conversation_returns(), $conv);
+        $this->assertFalse($conv['isfavourite']);
+    }
+
     /**
      * Test getting a conversation with no messages.
      */
@@ -6109,19 +6265,19 @@ 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;
-        list($user1, $user2, $user3, $user4, $user5, $user6, $user7) = [0, 1, 2, 3, 4, 5, 6];
+        list($user1, $user2, $user3, $user4, $user5, $user6, $user7, $user8) = [0, 1, 2, 3, 4, 5, 6, 7];
         $conversations = [
             [
                 'type' => $typeindividual,
                 'users' => [$user1, $user2],
-                'messages' => [$user1, $user2],
+                'messages' => [$user1, $user2, $user2],
                 'favourites' => [$user1],
                 'enabled' => null // Individual conversations cannot be disabled.
             ],
             [
                 'type' => $typeindividual,
                 'users' => [$user1, $user3],
-                'messages' => [$user1, $user1],
+                'messages' => [$user1, $user3, $user1],
                 'favourites' => [],
                 'enabled' => null // Individual conversations cannot be disabled.
             ],
@@ -6132,13 +6288,27 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'favourites' => [],
                 'enabled' => true
             ],
+            [
+                'type' => $typegroup,
+                'users' => [$user2, $user3, $user4],
+                'messages' => [$user2, $user3, $user4],
+                'favourites' => [],
+                'enabled' => true
+            ],
             [
                 'type' => $typegroup,
                 'users' => [$user6, $user7],
-                'messages' => [$user6, $user7],
+                'messages' => [$user6, $user7, $user7],
                 'favourites' => [$user6],
                 'enabled' => false
             ],
+            [
+                'type' => $typeindividual,
+                'users' => [$user8, $user8],
+                'messages' => [$user8, $user8],
+                'favourites' => [],
+                'enabled' => null // Individual conversations cannot be disabled.
+            ],
         ];
 
         return [
@@ -6147,20 +6317,28 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user5],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
                 ]],
                 'deletedusers' => []
             ],
-            'No individual conversations, 1 group conversation' => [
+            'No individual conversations, 2 group conversations' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user4],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
@@ -6169,31 +6347,43 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
                 'deletedusers' => []
             ],
-            '1 individual conversation, 1 group conversation' => [
+            '1 individual conversation, 2 group conversations' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user2],
-                'expected' => ['favourites' => 0, 'types' => [
+                '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 => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
-            '1 group conversation only' => [
+            '2 group conversations only' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user4],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
-                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
@@ -6202,7 +6392,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'deletemessagesuser' => $user1,
                 'deletemessages' => [0],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -6211,9 +6405,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
             'All conversation types, delete a message from individual non-favourited, messages remaining' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [2],
+                'deletemessages' => [3],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -6222,9 +6420,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
             'All conversation types, delete all messages from individual favourited, no messages remaining' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [0, 1],
+                'deletemessages' => [0, 1, 2],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -6233,9 +6435,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
             'All conversation types, delete all messages from individual non-favourited, no messages remaining' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [2, 3],
+                'deletemessages' => [3, 4, 5],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -6244,31 +6450,43 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
             'All conversation types, delete all messages from individual favourited, no messages remaining, different user' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [0, 1],
+                'deletemessages' => [0, 1, 2],
                 'arguments' => [$user2],
-                'expected' => ['favourites' => 0, 'types' => [
+                '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 => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
             'All conversation types, delete all messages from individual non-favourited, no messages remaining, different user' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [2, 3],
+                'deletemessages' => [3, 4, 5],
                 'arguments' => [$user3],
-                'expected' => ['favourites' => 0, 'types' => [
+                '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 => 2
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 2
                 ]],
                 'deletedusers' => []
             ],
             'All conversation types, delete some messages from group non-favourited, messages remaining,' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [4, 5],
+                'deletemessages' => [6, 7],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -6277,12 +6495,16 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
             'All conversation types, delete all messages from group non-favourited, no messages remaining,' => [
                 'conversationConfigs' => $conversations,
                 'deletemessagesuser' => $user1,
-                'deletemessages' => [4, 5, 6, 7],
+                'deletemessages' => [6, 7, 8, 9],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
                 'deletedusers' => []
             ],
             'All conversation types, another user soft deleted' => [
@@ -6290,7 +6512,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -6301,7 +6527,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user1],
-                'expected' => ['favourites' => 1, 'types' => [
+                'expectedcounts' => ['favourites' => 1, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
+                ]],
+                'expectedunreadcounts' => ['favourites' => 1, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 1,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 1
                 ]],
@@ -6312,7 +6542,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user6],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
                 ]],
@@ -6323,7 +6557,26 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 'deletemessagesuser' => null,
                 'deletemessages' => [],
                 'arguments' => [$user7],
-                'expected' => ['favourites' => 0, 'types' => [
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'deletedusers' => []
+            ],
+            'Conversation with self' => [
+                'conversationConfigs' => $conversations,
+                'deletemessagesuser' => null,
+                'deletemessages' => [],
+                'arguments' => [$user8],
+                'expectedcounts' => ['favourites' => 0, 'types' => [
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
+                    \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
+                ]],
+                'expectedunreadcounts' => ['favourites' => 0, 'types' => [
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP => 0
                 ]],
@@ -6340,7 +6593,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
      * @param int $deletemessagesuser The user who is deleting the messages
      * @param array $deletemessages The list of messages to delete (by index)
      * @param array $arguments Arguments for the count conversations function
-     * @param array $expected The expected result
+     * @param array $expectedcounts the expected conversation counts
+     * @param array $expectedunreadcounts the expected unread conversation counts
      * @param array $deletedusers the array of users to soft delete.
      */
     public function test_get_conversation_counts(
@@ -6348,12 +6602,14 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $deletemessagesuser,
         $deletemessages,
         $arguments,
-        $expected,
+        $expectedcounts,
+        $expectedunreadcounts,
         $deletedusers
     ) {
         $this->resetAfterTest();
         $generator = $this->getDataGenerator();
         $users = [
+            $generator->create_user(),
             $generator->create_user(),
             $generator->create_user(),
             $generator->create_user(),
@@ -6406,10 +6662,94 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $counts = core_message_external::get_conversation_counts(...$arguments);
         $counts = external_api::clean_returnvalue(core_message_external::get_conversation_counts_returns(), $counts);
 
-        $this->assertEquals($expected['favourites'], $counts['favourites']);
-        $this->assertEquals($expected['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL],
+        $this->assertEquals($expectedcounts['favourites'], $counts['favourites']);
+        $this->assertEquals($expectedcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL],
+            $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]);
+    }
+
+    /**
+     * Test the get_unread_conversation_counts() function.
+     *
+     * @dataProvider test_get_conversation_counts_test_cases()
+     * @param array $conversationconfigs Conversations to create
+     * @param int $deletemessagesuser The user who is deleting the messages
+     * @param array $deletemessages The list of messages to delete (by index)
+     * @param array $arguments Arguments for the count conversations function
+     * @param array $expectedcounts the expected conversation counts
+     * @param array $expectedunreadcounts the expected unread conversation counts
+     * @param array $deletedusers the list of users to soft-delete.
+     */
+    public function test_get_unread_conversation_counts(
+        $conversationconfigs,
+        $deletemessagesuser,
+        $deletemessages,
+        $arguments,
+        $expectedcounts,
+        $expectedunreadcounts,
+        $deletedusers
+    ) {
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+        $users = [
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user(),
+            $generator->create_user()
+        ];
+
+        $deleteuser = !is_null($deletemessagesuser) ? $users[$deletemessagesuser] : null;
+        $this->setUser($users[$arguments[0]]);
+        $arguments[0] = $users[$arguments[0]]->id;
+        $systemcontext = \context_system::instance();
+        $conversations = [];
+        $messageids = [];
+
+        foreach ($conversationconfigs as $config) {
+            $conversation = \core_message\api::create_conversation(
+                $config['type'],
+                array_map(function($userindex) use ($users) {
+                    return $users[$userindex]->id;
+                }, $config['users']),
+                null,
+                ($config['enabled'] ?? true)
+            );
+
+            foreach ($config['messages'] as $userfromindex) {
+                $userfrom = $users[$userfromindex];
+                $messageids[] = testhelper::send_fake_message_to_conversation($userfrom, $conversation->id);
+            }
+
+            foreach ($config['favourites'] as $userfromindex) {
+                $userfrom = $users[$userfromindex];
+                $usercontext = \context_user::instance($userfrom->id);
+                $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
+                $ufservice->create_favourite('core_message', 'message_conversations', $conversation->id, $systemcontext);
+            }
+
+            $conversations[] = $conversation;
+        }
+
+        foreach ($deletemessages as $messageindex) {
+            \core_message\api::delete_message($deleteuser->id, $messageids[$messageindex]);
+        }
+
+        foreach ($deletedusers as $deleteduser) {
+            delete_user($users[$deleteduser]);
+        }
+
+        $counts = core_message_external::get_unread_conversation_counts(...$arguments);
+        $counts = external_api::clean_returnvalue(core_message_external::get_unread_conversation_counts_returns(), $counts);
+
+        $this->assertEquals($expectedunreadcounts['favourites'], $counts['favourites']);
+        $this->assertEquals($expectedunreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL],
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL]);
-        $this->assertEquals($expected['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP],
+        $this->assertEquals($expectedunreadcounts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP],
             $counts['types'][\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP]);
     }
 }
diff --git a/pix/i/checkedcircle.png b/pix/i/checkedcircle.png
new file mode 100644 (file)
index 0000000..5501a02
Binary files /dev/null and b/pix/i/checkedcircle.png differ
diff --git a/pix/i/checkedcircle.svg b/pix/i/checkedcircle.svg
new file mode 100644 (file)
index 0000000..60aa48a
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>selected</title><path d="M8,16a8,8,0,1,1,8-8A8,8,0,0,1,8,16ZM13.19,5.83l-.95-.94a.65.65,0,0,0-.94,0L7.05,9.14,4.7,6.78a.65.65,0,0,0-.94,0l-.95.94a.68.68,0,0,0-.19.48.65.65,0,0,0,.19.47l3.77,3.77a.67.67,0,0,0,.95,0l5.66-5.66a.65.65,0,0,0,.19-.47A.68.68,0,0,0,13.19,5.83Z" fill="#999"/></svg>
\ No newline at end of file
diff --git a/pix/i/uncheckedcircle.png b/pix/i/uncheckedcircle.png
new file mode 100644 (file)
index 0000000..3966279
Binary files /dev/null and b/pix/i/uncheckedcircle.png differ
diff --git a/pix/i/uncheckedcircle.svg b/pix/i/uncheckedcircle.svg
new file mode 100644 (file)
index 0000000..d66f099
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>select 2</title><path d="M8,16A8,8,0,0,1,8,0H8A8,8,0,0,1,8,16ZM1.33,8A6.67,6.67,0,1,0,8,1.33,6.67,6.67,0,0,0,1.33,8Z" fill="#999"/></svg>
\ No newline at end of file
index 0d614c0..5ba00a6 100644 (file)
@@ -453,7 +453,7 @@ class moodle_content_writer implements content_writer {
 
                 $this->write_data($newshortpath, $variablecontent);
             } else {
-                $treekey[$shortpath] = 'No var';
+                $treekey[clean_param($shortpath, PARAM_PATH)] = 'No var';
             }
         }
         return [$tree, $treekey, $allfiles];
@@ -488,11 +488,11 @@ class moodle_content_writer implements content_writer {
             $url = clean_param($url, PARAM_PATH);
             $treeleaf->name = $file;
             $treeleaf->itemtype = 'item';
-            $gokey = $url . DIRECTORY_SEPARATOR . $file;
+            $gokey = clean_param($url . '/' . $file, PARAM_PATH);
             if (isset($treekey[$gokey]) && $treekey[$gokey] !== 'No var') {
                 $treeleaf->datavar = $treekey[$gokey];
             } else {
-                $treeleaf->url = new \moodle_url($url . DIRECTORY_SEPARATOR . $file);
+                $treeleaf->url = new \moodle_url($url . '/' . $file);
             }
         };
 
index fe36274..8661198 100644 (file)
@@ -94,9 +94,6 @@ $card-gutter : $card-deck-margin * 2;
     }
     .icon {
         margin-right: 0;
-        &.fa-star {
-            color: $blue;
-        }
     }
     .card .coursemenubtn {
         margin-top: -0.5rem;
index 80ef7d5..8f5c2e7 100644 (file)
@@ -2245,3 +2245,11 @@ $switch-transition: .2s all !default;
 .paged-content-page-container {
     min-height: 3.125rem;
 }
+
+.text-decoration-none {
+    text-decoration: none !important; /* stylelint-disable-line declaration-no-important */
+}
+
+.position-right {
+    right: 0 !important; /* stylelint-disable-line declaration-no-important */
+}
\ No newline at end of file
index af49c10..a83711e 100644 (file)
     #region-main {
         min-width: 100%;
         width: auto;
+        display: inline-flex;
         & > .card {
             width: auto;
             overflow-x: initial;
         }
+        div[role="main"] {
+            flex: 1 1 auto;
+        }
     }
     [data-region="blocks-column"] {
         width: 100%;
             cursor: pointer;
         }
     }
-    #page-header.row {
-        flex: 1 0 auto;
-    }
-    #page-wrapper #page {
-        flex-flow: wrap;
-    }
 }
 
 /**
index dc69552..3005682 100644 (file)
@@ -1269,6 +1269,13 @@ $message-drawer-width: 320px;
     .btn-link {
         color: inherit;
     }
+    .btn-link {
+        &:hover,
+        &:focus {
+            background-color: rgba($black, .035);
+            text-decoration: none;
+        }
+    }
 
     .icon {
         margin-right: 0;
@@ -1328,6 +1335,28 @@ $message-drawer-width: 320px;
             }
         }
     }
+    .list-group .list-group-item {
+        border-left: 0;
+        border-right: 0;
+        &:first-child {
+            border-top: 0;
+        }
+        &:last-child {
+            border-bottom: 0;
+        }
+    }
+    .last-message {
+        min-height: 1.5rem;
+    }
+    .section {
+        .collapsing {
+            overflow: hidden;
+        }
+    }
+
+    .lazy-load-list {
+        overflow-y: auto;
+    }
 }
 
 .dir-rtl {
index fac82ca..7791d6d 100644 (file)
@@ -10528,6 +10528,14 @@ div.editor_atto_toolbar button .icon {
 .paged-content-page-container {
   min-height: 3.125rem; }
 
+.text-decoration-none {
+  text-decoration: none !important;
+  /* stylelint-disable-line declaration-no-important */ }
+
+.position-right {
+  right: 0 !important;
+  /* stylelint-disable-line declaration-no-important */ }
+
 .icon {
   font-size: 16px;
   width: 16px;
@@ -11224,8 +11232,6 @@ div.editor_atto_toolbar button .icon {
 
 .block .block-cards .icon {
   margin-right: 0; }
-  .block .block-cards .icon.fa-star {
-    color: #1177d1; }
 
 .block .block-cards .card .coursemenubtn, .block .block-cards #page-enrol-users #filterform .coursemenubtn, #page-enrol-users .block .block-cards #filterform .coursemenubtn, .block .block-cards .que .history .coursemenubtn, .que .block .block-cards .history .coursemenubtn, .block .block-cards .userprofile .profile_tree section .coursemenubtn, .userprofile .profile_tree .block .block-cards section .coursemenubtn, .block .block-cards .groupinfobox .coursemenubtn, .block .block-cards .well .coursemenubtn {
   margin-top: -0.5rem; }
@@ -14131,6 +14137,9 @@ a.ygtvspacer:hover {
   .message-drawer a,
   .message-drawer .btn-link {
     color: inherit; }
+  .message-drawer .btn-link:hover, .message-drawer .btn-link:focus {
+    background-color: rgba(0, 0, 0, 0.035);
+    text-decoration: none; }
   .message-drawer .icon {
     margin-right: 0; }
   .message-drawer .overview-section-toggle .collapsed-icon-container {
@@ -14185,6 +14194,19 @@ a.ygtvspacer:hover {
       display: flex; }
   .message-drawer .view-conversation .content-message-container img {
     max-width: 100%; }
+  .message-drawer .list-group .list-group-item {
+    border-left: 0;
+    border-right: 0; }
+    .message-drawer .list-group .list-group-item:first-child {
+      border-top: 0; }
+    .message-drawer .list-group .list-group-item:last-child {
+      border-bottom: 0; }
+  .message-drawer .last-message {
+    min-height: 1.5rem; }
+  .message-drawer .section .collapsing {
+    overflow: hidden; }
+  .message-drawer .lazy-load-list {
+    overflow-y: auto; }
 
 .dir-rtl .message-drawer {
   box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.08); }
@@ -16155,10 +16177,13 @@ p.arrow_button {
 
 .path-grade-report-grader #region-main {
   min-width: 100%;
-  width: auto; }
+  width: auto;
+  display: inline-flex; }
   .path-grade-report-grader #region-main > .card, .path-grade-report-grader #page-enrol-users #region-main > #filterform, #page-enrol-users .path-grade-report-grader #region-main > #filterform, .path-grade-report-grader .que #region-main > .history, .que .path-grade-report-grader #region-main > .history, .path-grade-report-grader .userprofile .profile_tree #region-main > section, .userprofile .profile_tree .path-grade-report-grader #region-main > section, .path-grade-report-grader #region-main > .groupinfobox, .path-grade-report-grader #region-main > .well {
     width: auto;
     overflow-x: initial; }
+  .path-grade-report-grader #region-main div[role="main"] {
+    flex: 1 1 auto; }
 
 .path-grade-report-grader [data-region="blocks-column"] {
   width: 100%;
@@ -16372,12 +16397,6 @@ p.arrow_button {
 .path-grade-report-grader .gradeparent table .clickable {
   cursor: pointer; }
 
-.path-grade-report-grader #page-header.row {
-  flex: 1 0 auto; }
-
-.path-grade-report-grader #page-wrapper #page {
-  flex-flow: wrap; }
-
 /**
  * User report.
  */
index aa1937f..1812bf7 100644 (file)
@@ -14,8 +14,8 @@
 // For the white version of the icons, just add the .icon-white class:
 // <i class="icon-inbox icon-white"></i>
 
-[class^="icon-"]:not([class^="icon-size-"]),
-[class*=" icon-"]:not([class*=" icon-size-"]) {
+[class^="icon-"],
+[class*=" icon-"] {
   display: inline-block;
   width: 14px;
   height: 14px;
index 1645a32..2936d93 100644 (file)
 .position-relative {
     position: relative !important;
 }
+.align-self-start {
+    align-self: flex-start !important;
+}
 .align-self-center {
     align-self: center !important;
 }
         width: 24px !important;
     }
 }
-
+.text-muted {
+    .muted;
+}
 .dir-rtl {
     .dir-rtl-hide {
         display: none;
index 251435c..a712881 100644 (file)
@@ -2442,4 +2442,12 @@ h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions {
     100% {
         background-color: @grayLighter;
     }
+}
+
+.text-decoration-none {
+    text-decoration: none !important; /* stylelint-disable-line declaration-no-important */
+}
+
+.position-right {
+    right: 0 !important; /* stylelint-disable-line declaration-no-important */
 }
\ No newline at end of file
index fb52a7e..6533970 100644 (file)
         text-decoration: none;
         color: inherit;
     }
-
+    .btn-link {
+        &:hover,
+        &:focus {
+            background-color: darken(@wellBackground, 5%);
+            text-decoration: none;
+        }
+    }
+    .accordion-group .collapse.in {
+        overflow-y: auto;
+    }
     .message {
         p {
             margin: 0;
             }
         }
     }
+    .list-group {
+        .list-group-item {
+            border-top: 1px solid #e3e3e3;
+            border-bottom: 1px solid #e3e3e3;
+            margin-bottom: -1px;
+            &:first-child {
+                border-top: 0;
+            }
+            &:last-child {
+                border-bottom: 0;
+                margin-bottom: 0;
+            }
+        }
+    }
+    .last-message {
+        min-height: 20px;
+    }
+
+    .lazy-load-list {
+        overflow-y: auto;
+    }
 }
 
 @media (max-width: 480px) {
index 50a4d1a..541d332 100644 (file)
@@ -124,10 +124,13 @@ img.hide {
 // Bootstrap adds a background sprite to anything with classname starting "icon-".
 // we only want to load it if we use them in content.
 .icon-bar,
+.icon-no-margin,
 img.icon-post,
 img.icon-info,
 img.icon-warn,
-img.icon-pre {
+img.icon-pre,
+[class^="icon-size-"],
+[class*=" icon-size-"] {
     background-image: none;
 }
 
index 238ea14..0c28edb 100644 (file)
@@ -12,7 +12,6 @@ To update to the latest release of twitter bootstrap:
 * download the new less files and store them in less/bootstrap
 * Apply change in MDL-42195 (We don't want responsive images by default).
 * Apply change in MDL-48328 (We need to reset the width of the container directly, in ./less/bootstrap/navbar.less, using the calculated value found in ./less/bootstrap/mixin.less).
-* Apply change in MDL-64091 (We don't want icon-size-n classes to have the icon background image applied).
 * regenerate css files using grunt
 * update ./thirdpartylibs.xml
 
index 83a4ea0..4fbae66 100644 (file)
@@ -774,8 +774,8 @@ table th[class*="span"],
 .table-hover tbody tr.info:hover > td {
   background-color: #c4e3f3;
 }
-[class^="icon-"]:not([class^="icon-size-"]),
-[class*=" icon-"]:not([class*=" icon-size-"]) {
+[class^="icon-"],
+[class*=" icon-"] {
   display: inline-block;
   width: 14px;
   height: 14px;
index 2435772..cc303cf 100644 (file)
@@ -4823,6 +4823,14 @@ h3.sectionname .inplaceeditable.inplaceeditingon .editinstructions {
     background-color: #eee;
   }
 }
+.text-decoration-none {
+  text-decoration: none !important;
+  /* stylelint-disable-line declaration-no-important */
+}
+.position-right {
+  right: 0 !important;
+  /* stylelint-disable-line declaration-no-important */
+}
 /* admin.less */
 .formtable tbody th {
   font-weight: normal;
@@ -9176,6 +9184,14 @@ a.ygtvspacer:hover {
   text-decoration: none;
   color: inherit;
 }
+.message-drawer .btn-link:hover,
+.message-drawer .btn-link:focus {
+  background-color: #e8e8e8;
+  text-decoration: none;
+}
+.message-drawer .accordion-group .collapse.in {
+  overflow-y: auto;
+}
 .message-drawer .message p {
   margin: 0;
 }
@@ -9217,6 +9233,24 @@ a.ygtvspacer:hover {
 .message-drawer .view-conversation .content-message-container img {
   max-width: 100%;
 }
+.message-drawer .list-group .list-group-item {
+  border-top: 1px solid #e3e3e3;
+  border-bottom: 1px solid #e3e3e3;
+  margin-bottom: -1px;
+}
+.message-drawer .list-group .list-group-item:first-child {
+  border-top: 0;
+}
+.message-drawer .list-group .list-group-item:last-child {
+  border-bottom: 0;
+  margin-bottom: 0;
+}
+.message-drawer .last-message {
+  min-height: 20px;
+}
+.message-drawer .lazy-load-list {
+  overflow-y: auto;
+}
 @media (max-width: 480px) {
   .messaging-area-container .messaging-area .messages-area.editing .messages-header {
     height: 80px;
@@ -12557,8 +12591,8 @@ table th[class*="span"],
 .table-hover tbody tr.info:hover > td {
   background-color: #c4e3f3;
 }
-[class^="icon-"]:not([class^="icon-size-"]),
-[class*=" icon-"]:not([class*=" icon-size-"]) {
+[class^="icon-"],
+[class*=" icon-"] {
   display: inline-block;
   width: 14px;
   height: 14px;
@@ -16040,10 +16074,13 @@ img.hide {
   display: inherit;
 }
 .icon-bar,
+.icon-no-margin,
 img.icon-post,
 img.icon-info,
 img.icon-warn,
-img.icon-pre {
+img.icon-pre,
+[class^="icon-size-"],
+[class*=" icon-size-"] {
   background-image: none;
 }
 .loginbox.twocolumns .signuppanel,
@@ -22265,6 +22302,9 @@ ul.indented-list {
 .position-relative {
   position: relative !important;
 }
+.align-self-start {
+  align-self: flex-start !important;
+}
 .align-self-center {
   align-self: center !important;
 }
@@ -22333,6 +22373,9 @@ ul.indented-list {
   height: 24px !important;
   width: 24px !important;
 }
+.text-muted {
+  color: #999;
+}
 .dir-rtl .dir-rtl-hide {
   display: none;
 }
index abaff69..a8173b6 100644 (file)
@@ -39,7 +39,7 @@
 }}
 
 <div
-    class="message clickable d-flex flex-column p-2 {{#fromloggedinuser}}bg-secondary{{/fromloggedinuser}}{{^fromloggedinuser}}bg-white{{/fromloggedinuser}} rounded mb-2"
+    class="message clickable d-flex flex-column p-2 {{#fromloggedinuser}}bg-secondary{{/fromloggedinuser}}{{^fromloggedinuser}}bg-white{{/fromloggedinuser}} rounded mb-2 mt-2"
     data-region="message"
     data-message-id="{{id}}"
     aria-checked="false"
@@ -48,7 +48,7 @@
 >
     <div class="d-flex align-items-center {{^fromloggedinuser}}pb-2{{/fromloggedinuser}}">
         {{^fromloggedinuser}}
-        <div>
+        <div style="flex-shrink: 0">
             <img
                 class="rounded-circle"
                 src="{{{userfrom.profileimageurl}}}"
@@ -57,8 +57,8 @@
                 style="height: 30px"
             >
         </div>
-        <div class="pl-2 pr-2">
-            <h6 class="text-truncate m-0">{{userfrom.fullname}}</h6>
+        <div class="text-truncate pl-2 pr-2">
+            <h6 class="text-truncate m-0"><strong>{{userfrom.fullname}}</strong></h6>
         </div>
         {{/fromloggedinuser}}
         <div
@@ -67,8 +67,8 @@
         >
             {{formattedtime}}
         </div>
-        <span class="hidden ml-2 small text-primary" data-region="not-selected-icon">{{#pix}} i/completion-manual-n, core {{/pix}}</span>
-        <span class="hidden ml-2 small text-primary" data-region="selected-icon">{{#pix}} i/checked, core {{/pix}}</span>
+        <span class="hidden ml-2 small text-primary" data-region="not-selected-icon">{{#pix}} i/uncheckedcircle, core {{/pix}}</span>
+        <span class="hidden ml-2 small text-primary" data-region="selected-icon">{{#pix}} i/checkedcircle, core {{/pix}}</span>
     </div>
     <div>{{{text}}}</div>
 </div>
index bae82ee..d8568f4 100644 (file)
@@ -40,7 +40,7 @@
 
 <div class="d-flex p-3">
     <button
-        class="btn btn-link btn-icon my-1 icon-size-4"
+        class="btn btn-link btn-icon my-1 icon-size-4 ml-auto"
         data-action="delete-selected-messages"
         data-toggle="tooltip"
         data-placement="top"
@@ -50,4 +50,4 @@
         <span class="hidden" data-region="loading-icon-container">{{> core/loading }}</span>
         <span class="sr-only">{{#str}} deleteselectedmessages, core_message {{/str}}</span>
     </button>
-</div>
\ No newline at end of file
+</div>
index ae6f25b..6b341d8 100644 (file)
@@ -47,7 +47,7 @@
         </a>
     </div>
     <div class="d-flex text-truncate">
-        <a class="d-flex text-truncate" href="#" data-action="view-contact">
+        <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-contact">
             {{#imageurl}}
                 <div class="d-flex align-items-center">
                     <img
index 42071fc..e9d8a8e 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>
-            {{> core_message/message_drawer_icon_back }}
-        </a>
-    </div>
-    <div class="d-flex text-truncate">
-        <a class="d-flex text-truncate" href="#" data-action="view-group-info">
-            {{#imageurl}}
-                <div class="d-flex align-items-center">
+<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>
+                {{> core_message/message_drawer_icon_back }}
+            </a>
+        </div>
+        <div class="d-flex text-truncate">
+            <a class="d-flex text-truncate text-decoration-none" href="#" data-action="view-group-info">
+                {{#imageurl}}
                     <img
                         class="rounded-circle"
                         src="{{{.}}}"
                         aria-hidden="true"
                         style="height: 38px"
                     >
+                {{/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, core {{/pix}}
+                        </span>
+                    </div>
+                    <p class="m-0 text-truncate">{{subname}}</p>
                 </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, core {{/pix}}
-                    </span>
-                </div>
-                <p class="m-0 text-truncate">{{subname}}</p>
-                <p class="m-0 font-weight-light text-truncate">
-                    {{#str}} numparticipants, core_message, {{totalmembercount}} {{/str}}
-                </p>
-            </div>
-        </a>
-    </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">
-            <li data-action="view-group-info">
-                <a class="dropdown-item" href="#">
-                    {{#str}} groupinfo, core_message {{/str}}
-                </a>
-            </li>
-            <li class="{{#isfavourite}}hidden{{/isfavourite}} {{^showfavourite}}hidden{{/showfavourite}}" data-action="confirm-favourite">
-                <a class="dropdown-item" href="#">
-                    {{#str}} addtofavourites, core_message {{/str}}
-                </a>
-            </li>
-            <li class="{{^isfavourite}}hidden{{/isfavourite}} {{^showfavourite}}hidden{{/showfavourite}}" data-action="confirm-unfavourite">
-                <a class="dropdown-item" href="#">
-                    {{#str}} removefromfavourites, core_message {{/str}}
-                </a>
-            </li>
+            </a>
+        </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>
+            <ul class="dropdown-menu pull-right">
+                <li data-action="view-group-info">
+                    <a class="dropdown-item" href="#">
+                        {{#str}} groupinfo, core_message {{/str}}
+                    </a>
+                </li>
+                <li class="{{#isfavourite}}hidden{{/isfavourite}}" data-action="confirm-favourite">
+                    <a class="dropdown-item" href="#">
+                        {{#str}} addtofavourites, core_message {{/str}}
+                    </a>
+                </li>
+                <li class="{{^isfavourite}}hidden{{/isfavourite}}" data-action="confirm-unfavourite">
+                    <a class="dropdown-item" href="#">
+                        {{#str}} removefromfavourites, core_message {{/str}}
+                    </a>
+                </li>
+            </ul>
         </div>
     </div>
-</div>
\ No newline at end of file
+    <div class="d-flex">
+        <!-- These are just placeholder elements to align the text below correctly with the name text above -->
+        <div class="mr-2" style="opacity: 0; visibility: hidden" aria-hidden="true">{{> core_message/message_drawer_icon_back }}</div>
+        {{#imageurl}}<div aria-hidden="true" style="width: 38px"></div>{{/imageurl}}
+        <!-- End placeholders -->
+        <a class="text-decoration-none line-height-3 ml-2" href="#" data-action="view-group-info">
+            <small class="m-0 muted text-truncate">
+                {{#str}} numparticipants, core_message, {{totalmembercount}} {{/str}}
+            </small>
+        </a>
+    </div>
+</div>
index b4798a1..28ce1fe 100644 (file)
@@ -35,6 +35,7 @@
     adding ".in" to ".show"
     adding .accordion-group to the main container div
     adding expanded to the .accordion-group if required for toggle icons to work.
+    changing span.font-weight-bold to strong
 
     Example context (json):
     {}
             <span class="expanded-icon-container">
                 {{#pix}} t/expanded, core {{/pix}}
             </span>
-            {{$title}}{{/title}} (<span data-region="section-total-count">{{count.total}}</span>)
+            <strong>{{$title}}{{/title}}</strong>
+            <span class="hidden ml-1" data-region="section-total-count-container">
+                (<span data-region="section-total-count">{{count.total}}</span>)
+            </span>
             <span class="hidden ml-2" data-region="loading-icon-container">
                 {{> core/loading }}
             </span>
@@ -76,4 +80,4 @@
             data-parent="#message-drawer-view-overview-container"
         {{/rootattributes}}
     {{/ core_message/message_drawer_lazy_load_list }}
-</div>
\ No newline at end of file
+</div>
index 67a04fe..785ec28 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2018112500.00;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2018112500.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.