Merge branch 'MDL-64787_master-bump-python-package' of git://github.com/dmonllao...
authorAdrian Greeve <abgreeve@gmail.com>
Wed, 13 Mar 2019 06:05:14 +0000 (14:05 +0800)
committerAdrian Greeve <abgreeve@gmail.com>
Wed, 13 Mar 2019 06:05:14 +0000 (14:05 +0800)
48 files changed:
files/renderer.php
lang/en/message.php
lib/behat/classes/partial_named_selector.php
lib/classes/message/manager.php
lib/classes/output/icon_system_fontawesome.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/tests/messagelib_test.php
message/amd/build/message_drawer_events.min.js
message/amd/build/message_drawer_view_conversation.min.js
message/amd/build/message_drawer_view_conversation_constants.min.js
message/amd/build/message_drawer_view_conversation_patcher.min.js
message/amd/build/message_drawer_view_conversation_renderer.min.js
message/amd/build/message_drawer_view_conversation_state_manager.min.js
message/amd/build/message_drawer_view_overview_section.min.js
message/amd/build/message_repository.min.js
message/amd/src/message_drawer_events.js
message/amd/src/message_drawer_view_conversation.js
message/amd/src/message_drawer_view_conversation_constants.js
message/amd/src/message_drawer_view_conversation_patcher.js
message/amd/src/message_drawer_view_conversation_renderer.js
message/amd/src/message_drawer_view_conversation_state_manager.js
message/amd/src/message_drawer_view_overview_section.js
message/amd/src/message_repository.js
message/classes/api.php
message/classes/privacy/provider.php
message/externallib.php
message/templates/message_drawer_conversations_list.mustache
message/templates/message_drawer_view_conversation_header_content_type_private.mustache
message/templates/message_drawer_view_conversation_header_content_type_private_no_controls.mustache
message/templates/message_drawer_view_conversation_header_content_type_public.mustache
message/tests/api_test.php
message/tests/behat/behat_message.php
message/tests/behat/group_conversation.feature [new file with mode: 0644]
message/tests/externallib_test.php
message/tests/privacy_provider_test.php
pix/i/muted.png [new file with mode: 0755]
pix/i/muted.svg [new file with mode: 0755]
theme/boost/scss/moodle/filemanager.scss
theme/boost/style/moodle.css
theme/boost/tests/behat/group_conversation.feature [new file with mode: 0644]
theme/bootstrapbase/less/moodle/filemanager.less
theme/bootstrapbase/style/moodle.css
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_private_no_controls.mustache
theme/bootstrapbase/templates/core_message/message_drawer_view_conversation_header_content_type_public.mustache
version.php

index 5484ff5..f48feb7 100644 (file)
@@ -168,7 +168,7 @@ class core_files_renderer extends plugin_renderer_base {
         <div class="fp-reficons2"></div>
     </div>
     <div class="fp-filename-field">
-        <div class="fp-filename"></div>
+        <div class="fp-filename text-truncate"></div>
     </div>
     </a>
     <a class="fp-contextmenu" href="#">'.$this->pix_icon('i/menu', '▶').'</a>
@@ -335,7 +335,7 @@ class core_files_renderer extends plugin_renderer_base {
         <div class="fp-reficons2"></div>
     </div>
     <div class="fp-filename-field">
-        <p class="fp-filename"></p>
+        <p class="fp-filename text-truncate"></p>
     </div>
 </a>';
         return $rv;
index f9a045a..5d394d2 100644 (file)
@@ -107,6 +107,7 @@ $string['messagepreferences'] = 'Message preferences';
 $string['messages'] = 'Messages';
 $string['messagesselected:'] = 'Messages selected:';
 $string['messagingdatahasnotbeenmigrated'] = 'Your messages are temporarily unavailable due to upgrades in the messaging infrastructure. Please wait for them to be migrated.';
+$string['muteconversation'] = 'Mute';
 $string['newonlymsg'] = 'Show only new';
 $string['newmessage'] = 'New message';
 $string['newmessagesearch'] = 'Select or search for a contact to send a new message.';
@@ -162,6 +163,11 @@ $string['privacy:metadata:message_contact_requests'] = 'The list of contact requ
 $string['privacy:metadata:message_contact_requests:requesteduserid'] = 'The ID of the user who received the contact request';
 $string['privacy:metadata:message_contact_requests:timecreated'] = 'The time when the contact request was created';
 $string['privacy:metadata:message_contact_requests:userid'] = 'The ID of the user who sent the contact request';
+$string['privacy:metadata:message_conversation_actions'] = 'The list of conversation user actions';
+$string['privacy:metadata:message_conversation_actions:action'] = 'The action that was performed';
+$string['privacy:metadata:message_conversation_actions:conversationid'] = 'The ID of the conversation this action belongs to';
+$string['privacy:metadata:message_conversation_actions:timecreated'] = 'The time when the action was created';
+$string['privacy:metadata:message_conversation_actions:userid'] = 'The ID of the user who performed this action';
 $string['privacy:metadata:message_conversation_members'] = 'The list of users in a conversation';
 $string['privacy:metadata:message_conversation_members:conversationid'] = 'The ID of the conversation';
 $string['privacy:metadata:message_conversation_members:timecreated'] = 'The time when the member was created';
@@ -233,6 +239,7 @@ $string['unblockcontact'] = 'Unblock contact';
 $string['unblockuser'] = 'Unblock user';
 $string['unblockuserconfirm'] = 'Are you sure you want to unblock {$a}?';
 $string['unknownuser'] = 'Unknown user';
+$string['unmuteconversation'] = 'Unmute';
 $string['unreadnotification'] = 'Unread notification: {$a}';
 $string['unreadnewgroupconversationmessage'] = 'New message from {$a->name} in {$a->conversationname}';
 $string['unreadnewmessage'] = 'New message from {$a}';
index def514b..fb68a4c 100644 (file)
@@ -65,8 +65,6 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'fieldset' => 'fieldset',
         'icon' => 'icon',
         'list_item' => 'list_item',
-        'message_area_region' => 'message_area_region',
-        'message_area_region_content' => 'message_area_region_content',
         'question' => 'question',
         'region' => 'region',
         'section' => 'section',
@@ -90,13 +88,14 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
         'fieldset' => 'fieldset',
         'file' => 'file',
         'filemanager' => 'filemanager',
+        'group_message' => 'group_message',
+        'group_message_header' => 'group_message_header',
+        'group_message_member' => 'group_message_member',
+        'group_message_tab' => 'group_message_tab',
         'icon' => 'icon',
         'link' => 'link',
         'link_or_button' => 'link_or_button',
         'list_item' => 'list_item',
-        'message_area_action' => 'message_area_action',
-        'message_area_region' => 'message_area_region',
-        'message_area_region_content' => 'message_area_region_content',
         'optgroup' => 'optgroup',
         'option' => 'option',
         'question' => 'question',
@@ -151,6 +150,21 @@ XPATH
             and
         normalize-space(descendant::*[contains(concat(' ', normalize-space(@class), ' '), ' modal-header ')] = %locator%)
     ]
+XPATH
+        , 'group_message' => <<<XPATH
+        .//*[@data-conversation-id]//img[contains(@alt, %locator%)]/..
+XPATH
+        , 'group_message_header' => <<<XPATH
+        .//*[@data-region='message-drawer']//div[@data-region='header-container']//*[text()[contains(., %locator%)]]
+XPATH
+    , 'group_message_member' => <<<XPATH
+        .//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
+        //div[@class='list-group' and not(contains(@class, 'hidden'))]//*[text()[contains(., %locator%)]] |
+        .//*[@data-region='message-drawer']//div[@data-region='group-info-content-container']
+        //div[@data-region='empty-message-container' and not(contains(@class, 'hidden'))]//*[text()[contains(., %locator%)]]
+XPATH
+    , 'group_message_tab' => <<<XPATH
+        .//*[@data-region='message-drawer']//button[@data-toggle='collapse']//*[text()[contains(., %locator%)]]/..
 XPATH
         , 'icon' => <<<XPATH
 .//*[contains(concat(' ', normalize-space(@class), ' '), ' icon ') and ( contains(normalize-space(@title), %locator%))]
@@ -183,15 +197,6 @@ XPATH
 XPATH
         , 'form_row' => <<<XPATH
 .//*[self::label or self::div[contains(concat(' ', @class, ' '), ' fstaticlabel ')]][contains(., %locator%)]/ancestor::*[contains(concat(' ', @class, ' '), ' fitem ')]
-XPATH
-        , 'message_area_region' => <<<XPATH
-.//div[@data-region='message-drawer']/descendant::*[@data-region = %locator%]
-XPATH
-        , 'message_area_region_content' => <<<XPATH
-.//div[@data-region='message-drawer']/descendant::*[@data-region-content = %locator%]
-XPATH
-        , 'message_area_action' => <<<XPATH
-.//div[@data-region='message-drawer']/descendant::*[@data-action = %locator%]
 XPATH
         , 'autocomplete_selection' => <<<XPATH
 .//div[contains(concat(' ', normalize-space(@class), ' '), concat(' ', 'form-autocomplete-selection', ' '))]/span[@role='listitem'][contains(normalize-space(.), %locator%)]
index 5440a58..de30732 100644 (file)
@@ -84,13 +84,16 @@ class manager {
         // 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.*
+        $sql = "SELECT u.*, mca.id as ismuted
                   FROM {user} u
+             LEFT JOIN {message_conversation_actions} mca
+                    ON mca.userid = u.id AND mca.conversationid = ? AND mca.action = ?
                  WHERE u.id IN (
                           SELECT mcm.userid FROM {message_conversation_members} mcm
-                           WHERE mcm.conversationid = :convid
+                           WHERE mcm.conversationid = ?
                  )";
-        $members = $DB->get_records_sql($sql, ['convid' => $eventdata->convid]);
+        $members = $DB->get_records_sql($sql, [$eventdata->convid, \core_message\api::CONVERSATION_ACTION_MUTED,
+            $eventdata->convid]);
         if (empty($members)) {
             throw new \moodle_exception("Conversation has no members or does not exist.");
         }
@@ -138,7 +141,10 @@ class manager {
         foreach ($otherusers as $recipient) {
             // If this message was a legacy (1:1) message, then we use the userto.
             if ($legacymessage) {
+                $ismuted = $recipient->ismuted;
+
                 $recipient = $eventdata->userto;
+                $recipient->ismuted = $ismuted;
             }
 
             $usertoisrealuser = (\core_user::is_real_user($recipient->id) != false);
@@ -195,8 +201,9 @@ class manager {
 
             // Fill in the array of processors to be used based on default and user preferences.
             // This applies only to individual conversations. Messages to group conversations ignore processors.
+            // Do not process muted conversations.
             $processorlist = [];
-            if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+            if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL && !$recipient->ismuted) {
                 foreach ($processors as $processor) {
                     // Skip adding processors for internal user, if processor doesn't support sending message to internal user.
                     if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) {
index 8d9672f..f187601 100644 (file)
@@ -260,6 +260,7 @@ class icon_system_fontawesome extends icon_system_font {
             'core:i/moodle_host' => 'fa-graduation-cap',
             'core:i/moremenu' => 'fa-ellipsis-h',
             'core:i/move_2d' => 'fa-arrows',
+            'core:i/muted' => 'fa-microphone-slash',
             'core:i/navigationitem' => 'fa-fw',
             'core:i/ne_red_mark' => 'fa-remove',
             'core:i/new' => 'fa-bolt',
index 1cb47ee..fd1548f 100644 (file)
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20190122" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20190308" COMMENT="XMLDB file for core Moodle tables"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
 >
         <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
       </KEYS>
     </TABLE>
+    <TABLE NAME="message_conversation_actions" COMMENT="Stores all per-user actions on individual conversations">
+      <FIELDS>
+        <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
+        <FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="conversationid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="action" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+        <FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
+      </FIELDS>
+      <KEYS>
+        <KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+        <KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
+        <KEY NAME="conversationid" TYPE="foreign" FIELDS="conversationid" REFTABLE="message_conversations" REFFIELDS="id"/>
+      </KEYS>
+    </TABLE>
     <TABLE NAME="message_user_actions" COMMENT="Stores all per-user actions on individual messages">
       <FIELDS>
         <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
index e69b247..ea5f502 100644 (file)
@@ -874,6 +874,24 @@ $functions = array(
         'type' => 'write',
         'capabilities' => 'moodle/course:managegroups'
     ),
+    'core_message_mute_conversations' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'mute_conversations',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Mutes a list of conversations',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_message_unmute_conversations' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'unmute_conversations',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Unmutes a list of conversations',
+        'type' => 'write',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_message_block_user' => array(
         'classname' => 'core_message_external',
         'methodname' => 'block_user',
index 80a304d..a6b4ae8 100644 (file)
@@ -2780,5 +2780,31 @@ function xmldb_main_upgrade($oldversion) {
         upgrade_main_savepoint(true, 2019030700.01);
     }
 
+    if ($oldversion < 2019030800.00) {
+        // Define table 'message_conversation_actions' to be created.
+        // Note - I would have preferred 'message_conversation_user_actions' but due to Oracle we can't. Boo.
+        $table = new xmldb_table('message_conversation_actions');
+
+        // Adding fields to table 'message_conversation_actions'.
+        $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
+        $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('conversationid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('action', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+        $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
+
+        // Adding keys to table 'message_conversation_actions'.
+        $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
+        $table->add_key('userid', XMLDB_KEY_FOREIGN, ['userid'], 'user', ['id']);
+        $table->add_key('conversationid', XMLDB_KEY_FOREIGN, ['conversationid'], 'message_conversations', ['id']);
+
+        // Conditionally launch create table for 'message_conversation_actions'.
+        if (!$dbman->table_exists($table)) {
+            $dbman->create_table($table);
+        }
+
+        // Main savepoint reached.
+        upgrade_main_savepoint(true, 2019030800.00);
+    }
+
     return true;
 }
index 01ec3fb..489a485 100644 (file)
@@ -1118,6 +1118,51 @@ class core_messagelib_testcase extends advanced_testcase {
         $this->assertTrue($storedfileexists);
     }
 
+    public function test_send_message_when_muted() {
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        $userfrom = $this->getDataGenerator()->create_user();
+        $userto = $this->getDataGenerator()->create_user();
+
+        // Create a conversation between the users.
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $userfrom->id,
+                $userto->id
+            ]
+        );
+
+        $message = new \core\message\message();
+        $message->courseid = 1;
+        $message->component = 'moodle';
+        $message->name = 'instantmessage';
+        $message->userfrom = $userfrom;
+        $message->convid = $conversation->id;
+        $message->subject = 'message subject 1';
+        $message->fullmessage = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml = '<p>message body</p>';
+        $message->smallmessage = 'small message';
+        $message->notification = '0';
+
+        $sink = $this->redirectEmails();
+        message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $sink->clear();
+
+        // Mute the conversation.
+        \core_message\api::mute_conversation($userto->id, $conversation->id);
+
+        $sink = $this->redirectEmails();
+        message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(0, $emails);
+        $sink->clear();
+    }
+
     /**
      * Is a particular message type in the list of message types.
      * @param string $component
index 15244eb..6cf3713 100644 (file)
Binary files a/message/amd/build/message_drawer_events.min.js and b/message/amd/build/message_drawer_events.min.js differ
index 8399306..6d5e118 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation.min.js and b/message/amd/build/message_drawer_view_conversation.min.js differ
index afd8fe0..a45db35 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation_constants.min.js and b/message/amd/build/message_drawer_view_conversation_constants.min.js differ
index a7d2929..7ec4cfd 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation_patcher.min.js and b/message/amd/build/message_drawer_view_conversation_patcher.min.js differ
index cf35154..b23ddc6 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation_renderer.min.js and b/message/amd/build/message_drawer_view_conversation_renderer.min.js differ
index 313aa09..c0f4082 100644 (file)
Binary files a/message/amd/build/message_drawer_view_conversation_state_manager.min.js and b/message/amd/build/message_drawer_view_conversation_state_manager.min.js differ
index 0154211..48c4b7f 100644 (file)
Binary files a/message/amd/build/message_drawer_view_overview_section.min.js and b/message/amd/build/message_drawer_view_overview_section.min.js differ
index 9f480a6..999ba17 100644 (file)
Binary files a/message/amd/build/message_repository.min.js and b/message/amd/build/message_repository.min.js differ
index 002d218..fa5ce01 100644 (file)
@@ -35,7 +35,9 @@ define([], function() {
         CONVERSATION_DELETED: 'message-drawer-conversation-deleted',
         CONVERSATION_READ: 'message-drawer-conversation-read',
         CONVERSATION_SET_FAVOURITE: 'message-drawer-conversation-set-favourite',
+        CONVERSATION_SET_MUTED: 'message-drawer-conversation-set-muted',
         CONVERSATION_UNSET_FAVOURITE: 'message-drawer-conversation-unset-favourite',
+        CONVERSATION_UNSET_MUTED: 'message-drawer-conversation-unset-muted',
         PREFERENCES_UPDATED: 'message-drawer-preferences-updated',
         ROUTE_CHANGED: 'message-drawer-route-change',
         SHOW: 'message-drawer-show',
index 486ee7c..8b8949b 100644 (file)
@@ -240,6 +240,7 @@ function(
             subname: state.subname,
             imageUrl: state.imageUrl,
             isFavourite: state.isFavourite,
+            isMuted: state.isMuted,
             type: state.type,
             totalMemberCount: state.totalMemberCount,
             loggedInUserId: state.loggedInUserId,
@@ -328,6 +329,7 @@ function(
         newState = StateManager.setImageUrl(newState, imageUrl);
         newState = StateManager.setTotalMemberCount(newState, conversation.membercount);
         newState = StateManager.setIsFavourite(newState, conversation.isfavourite);
+        newState = StateManager.setIsMuted(newState, conversation.ismuted);
         newState = StateManager.addMessages(newState, conversation.messages);
         return newState;
     };
@@ -795,6 +797,50 @@ function(
             });
     };
 
+    /**
+     * Set the current conversation as a muted conversation.
+     *
+     * @return {Promise} Renderer promise.
+     */
+    var setMuted = function() {
+        var userId = viewState.loggedInUserId;
+        var conversationId = viewState.id;
+
+        return Repository.setMutedConversations(userId, [conversationId])
+            .then(function() {
+                var newState = StateManager.setIsMuted(viewState, true);
+                return render(newState);
+            })
+            .then(function() {
+                return PubSub.publish(
+                    MessageDrawerEvents.CONVERSATION_SET_MUTED,
+                    formatConversationForEvent(viewState)
+                );
+            });
+    };
+
+    /**
+     * Unset the current conversation as a muted conversation.
+     *
+     * @return {Promise} Renderer promise.
+     */
+    var unsetMuted = function() {
+        var userId = viewState.loggedInUserId;
+        var conversationId = viewState.id;
+
+        return Repository.unsetMutedConversations(userId, [conversationId])
+            .then(function() {
+                var newState = StateManager.setIsMuted(viewState, false);
+                return render(newState);
+            })
+            .then(function() {
+                return PubSub.publish(
+                    MessageDrawerEvents.CONVERSATION_UNSET_MUTED,
+                    formatConversationForEvent(viewState)
+                );
+            });
+    };
+
     /**
      * Tell the statemanager there is a request to delete the selected messages
      * and run the renderer to show confirm delete messages dialogue.
@@ -1211,6 +1257,29 @@ function(
 
     /**
      * Show the view group info page.
+     * Set this conversation as muted.
+     *
+     * @param {Object} e Element this event handler is called on.
+     * @param {Object} data Data for this event.
+     */
+    var handleSetMuted = function(e, data) {
+        setMuted().catch(Notification.exception);
+        data.originalEvent.preventDefault();
+    };
+
+    /**
+     * Unset this conversation as muted.
+     *
+     * @param {Object} e Element this event handler is called on.
+     * @param {Object} data Data for this event.
+     */
+    var handleUnsetMuted = function(e, data) {
+        unsetMuted().catch(Notification.exception);
+        data.originalEvent.preventDefault();
+    };
+
+    /**
+     * Show the view contact page.
      *
      * @param {String} namespace Unique identifier for the Routes
      * @return {Function} View group info handler.
@@ -1254,7 +1323,9 @@ function(
             [SELECTORS.ACTION_VIEW_CONTACT, generateHandleViewContact(namespace)],
             [SELECTORS.ACTION_VIEW_GROUP_INFO, generateHandleViewGroupInfo(namespace)],
             [SELECTORS.ACTION_CONFIRM_FAVOURITE, handleSetFavourite],
+            [SELECTORS.ACTION_CONFIRM_MUTE, handleSetMuted],
             [SELECTORS.ACTION_CONFIRM_UNFAVOURITE, handleUnsetFavourite],
+            [SELECTORS.ACTION_CONFIRM_UNMUTE, handleUnsetMuted]
         ];
         var bodyActivateHandlers = [
             [SELECTORS.ACTION_CANCEL_CONFIRM, generateConfirmActionHandler(cancelRequest)],
index 83507c0..3bd84c8 100644 (file)
@@ -31,9 +31,11 @@ define([], function() {
         ACTION_CONFIRM_DELETE_SELECTED_MESSAGES: '[data-action="confirm-delete-selected-messages"]',
         ACTION_CONFIRM_DELETE_CONVERSATION: '[data-action="confirm-delete-conversation"]',
         ACTION_CONFIRM_FAVOURITE: '[data-action="confirm-favourite"]',
+        ACTION_CONFIRM_MUTE: '[data-action="confirm-mute"]',
         ACTION_CONFIRM_UNFAVOURITE: '[data-action="confirm-unfavourite"]',
         ACTION_CONFIRM_REMOVE_CONTACT: '[data-action="confirm-remove-contact"]',
         ACTION_CONFIRM_UNBLOCK: '[data-action="confirm-unblock"]',
+        ACTION_CONFIRM_UNMUTE: '[data-action="confirm-unmute"]',
         ACTION_DECLINE_CONTACT_REQUEST: '[data-action="decline-contact-request"]',
         ACTION_REQUEST_ADD_CONTACT: '[data-action="request-add-contact"]',
         ACTION_REQUEST_BLOCK: '[data-action="request-block"]',
@@ -74,6 +76,7 @@ define([], function() {
         MESSAGES_SELECTED_COUNT: '[data-region="message-selected-court"]',
         MESSAGE_TEXT_AREA: '[data-region="send-message-txt"]',
         MORE_MESSAGES_LOADING_ICON_CONTAINER: '[data-region="more-messages-loading-icon-container"]',
+        MUTED_ICON_CONTAINER: '[data-region="muted-icon-container"]',
         PLACEHOLDER_CONTAINER: '[data-region="placeholder-container"]',
         SEND_MESSAGE_BUTTON: '[data-action="send-message"]',
         SEND_MESSAGE_ICON_CONTAINER: '[data-region="send-icon-container"]',
index dd7556a..43ea13c 100644 (file)
@@ -298,6 +298,7 @@ function(
                     totalmembercount: newState.totalMemberCount,
                     imageurl: newState.imageUrl,
                     isfavourite: newState.isFavourite,
+                    ismuted: newState.isMuted,
                     // Don't show favouriting if we don't have a conversation.
                     showfavourite: newState.id !== null,
                     userid: newOtherUser.id,
@@ -336,6 +337,7 @@ function(
                     totalmembercount: newState.totalMemberCount,
                     imageurl: newState.imageUrl,
                     isfavourite: newState.isFavourite,
+                    ismuted: newState.isMuted,
                     // Don't show favouriting if we don't have a conversation.
                     showfavourite: newState.id !== null
                 }
@@ -646,6 +648,39 @@ function(
         }
     };
 
+    /**
+     * Check if there are any changes the conversation muted state.
+     *
+     * @param  {Object} state The current state.
+     * @param  {Object} newState The new state.
+     * @return {string|null}
+     */
+    var buildIsMuted = function(state, newState) {
+        var oldIsMuted = state.isMuted;
+        var newIsMuted = newState.isMuted;
+
+        if (state.id === null && newState.id === null) {
+            // The conversation isn't yet created so don't change anything.
+            return null;
+        } else if (state.id === null && newState.id !== null) {
+            // The conversation was created so we can show the mute button.
+            return 'show-mute';
+        } else if (state.id !== null && newState.id === null) {
+            // We're changing from a created conversation to a new conversation so hide
+            // the muting functionality for now.
+            return 'hide';
+        } else if (oldIsMuted == newIsMuted) {
+            // No change.
+            return null;
+        } else if (!oldIsMuted && newIsMuted) {
+            return 'show-unmute';
+        } else if (oldIsMuted && !newIsMuted) {
+            return 'show-mute';
+        } else {
+            return null;
+        }
+    };
+
     /**
      * Check if there are any changes in the contact status of the current user
      * and other user.
@@ -1130,7 +1165,8 @@ function(
                 confirmDeleteSelectedMessages: buildConfirmDeleteSelectedMessages,
                 inEditMode: buildInEditMode,
                 selectedMessages: buildSelectedMessages,
-                isFavourite: buildIsFavourite
+                isFavourite: buildIsFavourite,
+                isMuted: buildIsMuted
             }
         };
         // These build functions are only applicable to private conversations.
index 5e5cf90..d0b67f2 100644 (file)
@@ -1248,6 +1248,39 @@ function(
         }
     };
 
+    /**
+     * Show or hide the mute / unmute option in the header dropdown menu
+     * and the muted icon in the header title.
+     *
+     * @param {Object} header The header container element.
+     * @param {Object} body The body container element.
+     * @param {Object} footer The footer container element.
+     * @param {string} state The state of the conversation as defined by the patcher.
+     */
+    var renderIsMuted = function(header, body, footer, state) {
+        var muteIcon = header.find(SELECTORS.MUTED_ICON_CONTAINER);
+        var setMuted = header.find(SELECTORS.ACTION_CONFIRM_MUTE);
+        var unsetMuted = header.find(SELECTORS.ACTION_CONFIRM_UNMUTE);
+
+        switch (state) {
+            case 'hide':
+                muteIcon.addClass('hidden');
+                setMuted.addClass('hidden');
+                unsetMuted.addClass('hidden');
+                break;
+            case 'show-mute':
+                muteIcon.addClass('hidden');
+                setMuted.removeClass('hidden');
+                unsetMuted.addClass('hidden');
+                break;
+            case 'show-unmute':
+                muteIcon.removeClass('hidden');
+                setMuted.addClass('hidden');
+                unsetMuted.removeClass('hidden');
+                break;
+        }
+    };
+
     /**
      * Show or hide the add / remove user as contact option in the header dropdown menu.
      *
@@ -1476,6 +1509,7 @@ function(
                 isBlocked: renderIsBlocked,
                 isContact: renderIsContact,
                 isFavourite: renderIsFavourite,
+                isMuted: renderIsMuted,
                 loadingConfirmAction: renderLoadingConfirmAction,
                 inEditMode: renderInEditMode
             },
index 0340b45..98ed99e 100644 (file)
@@ -121,6 +121,7 @@ define(['jquery'], function($) {
             totalMemberCount: null,
             imageUrl: null,
             isFavourite: null,
+            isMuted: null,
             members: {},
             messages: [],
             hasTriedToLoadMessages: false,
@@ -347,6 +348,19 @@ define(['jquery'], function($) {
         return newState;
     };
 
+    /**
+     * Set whether the conversation is a muted conversation.
+     *
+     * @param  {Object} state Current state.
+     * @param  {bool} isMuted If it's muted.
+     * @return {Object} New state.
+     */
+    var setIsMuted = function(state, isMuted) {
+        var newState = cloneState(state);
+        newState.isMuted = isMuted;
+        return newState;
+    };
+
     /**
      * Set the total member count.
      *
@@ -659,6 +673,7 @@ define(['jquery'], function($) {
         setSubname: setSubname,
         setType: setType,
         setIsFavourite: setIsFavourite,
+        setIsMuted: setIsMuted,
         setTotalMemberCount: setTotalMemberCount,
         setImageUrl: setImageUrl,
         setLoadingConfirmAction: setLoadingConfirmAction,
index 8e39af3..bf32f2a 100644 (file)
@@ -58,6 +58,7 @@ function(
         BLOCKED_ICON_CONTAINER: '[data-region="contact-icon-blocked"]',
         LAST_MESSAGE: '[data-region="last-message"]',
         LAST_MESSAGE_DATE: '[data-region="last-message-date"]',
+        MUTED_ICON_CONTAINER: '[data-region="muted-icon-container"]',
         UNREAD_COUNT: '[data-region="unread-count"]',
         SECTION_TOTAL_COUNT: '[data-region="section-total-count"]',
         SECTION_TOTAL_COUNT_CONTAINER: '[data-region="section-total-count-container"]',
@@ -169,6 +170,7 @@ function(
                 name: conversation.name,
                 subname: conversation.subname,
                 unreadcount: conversation.unreadcount,
+                ismuted: conversation.ismuted,
                 lastmessagedate: lastMessage ? lastMessage.timecreated : null,
                 sentfromcurrentuser: lastMessage ? lastMessage.useridfrom == userId : null,
                 lastmessage: lastMessage ? $(lastMessage.text).text() || lastMessage.text : null
@@ -334,6 +336,24 @@ function(
         return root.find('[data-user-id="' + userId + '"]');
     };
 
+    /**
+     * Show the conversation is muted icon.
+     *
+     * @param  {Object} conversationElement The conversation element.
+     */
+    var muteConversation = function(conversationElement) {
+        conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).removeClass('hidden');
+    };
+
+    /**
+     * Hide the conversation is muted icon.
+     *
+     * @param  {Object} conversationElement The conversation element.
+     */
+    var unmuteConversation = function(conversationElement) {
+        conversationElement.find(SELECTORS.MUTED_ICON_CONTAINER).addClass('hidden');
+    };
+
     /**
      * Show the contact is blocked icon.
      *
@@ -515,11 +535,28 @@ function(
 
         PubSub.subscribe(MessageDrawerEvents.CONTACT_UNBLOCKED, function(userId) {
             var conversationElement = getConversationElementFromUserId(root, userId);
+
             if (conversationElement.length) {
                 unblockContact(conversationElement);
             }
         });
 
+        PubSub.subscribe(MessageDrawerEvents.CONVERSATION_SET_MUTED, function(conversation) {
+            var conversationId = conversation.id;
+            var conversationElement = getConversationElement(root, conversationId);
+            if (conversationElement.length) {
+                muteConversation(conversationElement);
+            }
+        });
+
+        PubSub.subscribe(MessageDrawerEvents.CONVERSATION_UNSET_MUTED, function(conversation) {
+            var conversationId = conversation.id;
+            var conversationElement = getConversationElement(root, conversationId);
+            if (conversationElement.length) {
+                unmuteConversation(conversationElement);
+            }
+        });
+
         PubSub.subscribe(MessageDrawerEvents.CONVERSATION_NEW_LAST_MESSAGE, function(conversation) {
             if (
                 (type && conversation.type != type) ||
index a7da2f0..24b2349 100644 (file)
@@ -905,6 +905,42 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
         return Ajax.call([request])[0];
     };
 
+    /**
+     * Set a list of conversations to set as muted for the given user.
+     *
+     * @param {int} userId The user id
+     * @param {array} conversationIds List of conversation ids to set as favourite
+     * @return {object} jQuery promise
+     */
+    var setMutedConversations = function(userId, conversationIds) {
+        var request = {
+            methodname: 'core_message_mute_conversations',
+            args: {
+                userid: userId,
+                conversationids: conversationIds
+            }
+        };
+        return Ajax.call([request])[0];
+    };
+
+    /**
+     * Set a list of conversations to unset as muted for the given user.
+     *
+     * @param {int} userId The user id
+     * @param {array} conversationIds List of conversation ids to unset as favourite
+     * @return {object} jQuery promise
+     */
+    var unsetMutedConversations = function(userId, conversationIds) {
+        var request = {
+            methodname: 'core_message_unmute_conversations',
+            args: {
+                userid: userId,
+                conversationids: conversationIds
+            }
+        };
+        return Ajax.call([request])[0];
+    };
+
     /**
      * Get a list of user's member info.
      *
@@ -1060,7 +1096,9 @@ define(['jquery', 'core/ajax', 'core/notification'], function($, Ajax, Notificat
         getConversations: getConversations,
         getConversationMembers: getConversationMembers,
         setFavouriteConversations: setFavouriteConversations,
+        setMutedConversations: setMutedConversations,
         unsetFavouriteConversations: unsetFavouriteConversations,
+        unsetMutedConversations: unsetMutedConversations,
         getMemberInfo: getMemberInfo,
         markAllConversationMessagesAsRead: markAllConversationMessagesAsRead,
         getUserMessagePreferences: getUserMessagePreferences,
index b15a97e..9376f5b 100644 (file)
@@ -48,6 +48,11 @@ class api {
      */
     const MESSAGE_ACTION_DELETED = 2;
 
+    /**
+     * The action for reading a message.
+     */
+    const CONVERSATION_ACTION_MUTED = 1;
+
     /**
      * The privacy setting for being messaged by anyone within courses user is member of.
      */
@@ -545,7 +550,7 @@ class api {
 
         $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
                        m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagehtml, m.timecreated, mc.component,
-                       mc.itemtype, mc.itemid, mc.contextid
+                       mc.itemtype, mc.itemid, mc.contextid, mca.action as ismuted
                   FROM {message_conversations} mc
             INNER JOIN {message_conversation_members} mcm
                     ON (mcm.conversationid = mc.id AND mcm.userid = :userid3)
@@ -569,12 +574,15 @@ class api {
                     ON lastmessage.conversationid = mc.id
             LEFT JOIN {messages} m
                    ON m.id = lastmessage.messageid
+            LEFT JOIN {message_conversation_actions} mca
+                   ON (mca.conversationid = mc.id AND mca.userid = :userid4 AND mca.action = :convaction)
                 WHERE mc.id IS NOT NULL
                   AND mc.enabled = 1 $typesql $favouritesql
               ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";
 
         $params = array_merge($favouriteparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
-            'userid2' => $userid, 'userid3' => $userid, 'convtype' => $type]);
+            'userid2' => $userid, 'userid3' => $userid, 'userid4' => $userid, 'convaction' => self::CONVERSATION_ACTION_MUTED,
+            'convtype' => $type]);
         $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
 
         $conversations = [];
@@ -790,6 +798,7 @@ class api {
             $conv->isfavourite = in_array($conv->id, $favouriteconversationids);
             $conv->isread = isset($unreadcounts[$conv->id]) ? false : true;
             $conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null;
+            $conv->ismuted = $conversation->ismuted ? true : false;
             $conv->members = $members[$conv->id];
 
             // Add the most recent message information.
@@ -950,6 +959,12 @@ class api {
 
         $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);
 
+        $ismuted = false;
+        if ($DB->record_exists('message_conversation_actions', ['userid' => $userid,
+                'conversationid' => $conversationid, 'action' => self::CONVERSATION_ACTION_MUTED])) {
+            $ismuted = true;
+        }
+
         return (object) [
             'id' => $conversation->id,
             'name' => $conversation->name,
@@ -960,6 +975,7 @@ class api {
             'isfavourite' => $isfavourite,
             'isread' => empty($unreadcount),
             'unreadcount' => $unreadcount,
+            'ismuted' => $ismuted,
             'members' => $members,
             'messages' => $messages['messages']
         ];
@@ -3133,4 +3149,59 @@ class api {
 
         return $counts;
     }
+
+    /**
+     * Handles muting a conversation.
+     *
+     * @param int $userid The id of the user
+     * @param int $conversationid The id of the conversation
+     */
+    public static function mute_conversation(int $userid, int $conversationid) : void {
+        global $DB;
+
+        $mutedconversation = new \stdClass();
+        $mutedconversation->userid = $userid;
+        $mutedconversation->conversationid = $conversationid;
+        $mutedconversation->action = self::CONVERSATION_ACTION_MUTED;
+        $mutedconversation->timecreated = time();
+
+        $DB->insert_record('message_conversation_actions', $mutedconversation);
+    }
+
+    /**
+     * Handles unmuting a conversation.
+     *
+     * @param int $userid The id of the user
+     * @param int $conversationid The id of the conversation
+     */
+    public static function unmute_conversation(int $userid, int $conversationid) : void {
+        global $DB;
+
+        $DB->delete_records('message_conversation_actions',
+            [
+                'userid' => $userid,
+                'conversationid' => $conversationid,
+                'action' => self::CONVERSATION_ACTION_MUTED
+            ]
+        );
+    }
+
+    /**
+     * Checks whether a conversation is muted or not.
+     *
+     * @param int $userid The id of the user
+     * @param int $conversationid The id of the conversation
+     * @return bool Whether or not the conversation is muted or not
+     */
+    public static function is_conversation_muted(int $userid, int $conversationid) : bool {
+        global $DB;
+
+        return $DB->record_exists('message_conversation_actions',
+            [
+                'userid' => $userid,
+                'conversationid' => $conversationid,
+                'action' => self::CONVERSATION_ACTION_MUTED
+            ]
+        );
+    }
 }
index 8e5441b..ee90f5c 100644 (file)
@@ -99,6 +99,16 @@ class provider implements
             'privacy:metadata:message_conversation_members'
         );
 
+        $items->add_database_table(
+            'message_conversation_actions',
+            [
+                'conversationid' => 'privacy:metadata:message_conversation_actions:conversationid',
+                'userid' => 'privacy:metadata:message_conversation_actions:userid',
+                'timecreated' => 'privacy:metadata:message_conversation_actions:timecreated',
+            ],
+            'privacy:metadata:message_conversation_actions'
+        );
+
         $items->add_database_table(
             'message_contacts',
             [
@@ -205,6 +215,8 @@ class provider implements
         // It is enough to add the user's context as soon as we come to the conclusion that the user has some data.
         // Also, the order of checking is sorted by the probability of occurrence (just by guess).
         // There is no need to check the message_user_actions table, as there needs to be a message in order to be a message action.
+        // There is no need to check the message_conversation_actions table, as there needs to be a conversation in order to
+        // be a conversation action.
         // So, checking messages table would suffice.
 
         $hasdata = false;
@@ -258,6 +270,8 @@ class provider implements
         // It is enough to add the user's context as soon as we come to the conclusion that the user has some data.
         // Also, the order of checking is sorted by the probability of occurrence (just by guess).
         // There is no need to check the message_user_actions table, as there needs to be a message in order to be a message action.
+        // There is no need to check the message_conversation_actions table, as there needs to be a conversation in order to
+        // be a conversation action.
         // So, checking messages table would suffice.
 
         $hasdata = false;
@@ -568,6 +582,7 @@ class provider implements
 
             // Delete members and conversations.
             $DB->delete_records_list('message_conversation_members', 'conversationid', $conversationids);
+            $DB->delete_records_list('message_conversation_actions', 'conversationid', $conversationids);
             $DB->delete_records_list('message_conversations', 'id', $conversationids);
         }
     }
@@ -673,6 +688,9 @@ class provider implements
             // Reuse the $params var because it contains the useridparams and the conversationids.
             $DB->delete_records_select('message_conversation_members', $sql, $params);
 
+            // Delete any conversation actions.
+            $DB->delete_records_select('message_conversation_actions', $sql, $params);
+
             // Delete the favourite conversations.
             $userlist = new \core_privacy\local\request\approved_userlist($context, 'core_message', $userids);
             \core_favourites\privacy\provider::delete_favourites_for_userlist(
@@ -763,6 +781,9 @@ class provider implements
             // Reuse the $params var because it contains the userid and the conversationids.
             $DB->delete_records_select('message_conversation_members', $sql, $params);
 
+            // Delete any conversation actions.
+            $DB->delete_records_select('message_conversation_actions', $sql, $params);
+
             // Delete the favourite conversations.
             if (empty($contextids) && empty($component) && empty($itemtype) && empty($itemid)) {
                 // Favourites for individual conversations are stored into the user context.
@@ -974,6 +995,20 @@ class provider implements
                 // If the conversation has been favorited by the user, include it in the export.
                 writer::with_context($context)->export_related_data($subcontext, 'starred', (object) $conversationfavourite);
             }
+
+            // Check if the conversation was muted.
+            $params = [
+                'userid' => $userid,
+                'conversationid' => $conversation->id,
+                'action' => \core_message\api::CONVERSATION_ACTION_MUTED
+            ];
+            if ($mca = $DB->get_record('message_conversation_actions', $params)) {
+                $mcatostore = [
+                    'muted' => transform::yesno(true),
+                    'timecreated' => transform::datetime($mca->timecreated),
+                ];
+                writer::with_context($context)->export_related_data($subcontext, 'muted', (object) $mcatostore);
+            }
         }
     }
 
index a779b98..9d10760 100644 (file)
@@ -410,6 +410,125 @@ class core_message_external extends external_api {
         return null;
     }
 
+    /**
+     * Mute conversations parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function mute_conversations_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user who is blocking'),
+                'conversationids' => new external_multiple_structure(
+                    new external_value(PARAM_INT, 'id of the conversation', VALUE_REQUIRED)
+                ),
+            ]
+        );
+    }
+
+    /**
+     * Mutes conversations.
+     *
+     * @param int $userid The id of the user who is blocking
+     * @param array $conversationids The list of conversations being muted
+     * @return external_description
+     */
+    public static function mute_conversations(int $userid, array $conversationids) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $params = ['userid' => $userid, 'conversationids' => $conversationids];
+        $params = self::validate_parameters(self::mute_conversations_parameters(), $params);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        foreach ($params['conversationids'] as $conversationid) {
+            if (!\core_message\api::is_conversation_muted($params['userid'], $conversationid)) {
+                \core_message\api::mute_conversation($params['userid'], $conversationid);
+            }
+        }
+
+        return [];
+    }
+
+    /**
+     * Mute conversations return description.
+     *
+     * @return external_description
+     */
+    public static function mute_conversations_returns() {
+        return new external_warnings();
+    }
+
+    /**
+     * Unmute conversations parameters description.
+     *
+     * @return external_function_parameters
+     */
+    public static function unmute_conversations_parameters() {
+        return new external_function_parameters(
+            [
+                'userid' => new external_value(PARAM_INT, 'The id of the user who is unblocking'),
+                'conversationids' => new external_multiple_structure(
+                    new external_value(PARAM_INT, 'id of the conversation', VALUE_REQUIRED)
+                ),
+            ]
+        );
+    }
+
+    /**
+     * Unmute conversations.
+     *
+     * @param int $userid The id of the user who is unblocking
+     * @param array $conversationids The list of conversations being muted
+     */
+    public static function unmute_conversations(int $userid, array $conversationids) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Validate context.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $params = ['userid' => $userid, 'conversationids' => $conversationids];
+        $params = self::validate_parameters(self::unmute_conversations_parameters(), $params);
+
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
+        foreach ($params['conversationids'] as $conversationid) {
+            \core_message\api::unmute_conversation($params['userid'], $conversationid);
+        }
+
+        return [];
+    }
+
+    /**
+     * Unmute conversations return description.
+     *
+     * @return external_description
+     */
+    public static function unmute_conversations_returns() {
+        return new external_warnings();
+    }
+
     /**
      * Block user parameters description.
      *
@@ -1123,7 +1242,8 @@ class core_message_external extends external_api {
                 'imageurl' => new external_value(PARAM_URL, 'A link to the conversation picture, if set', VALUE_DEFAULT, null),
                 'type' => new external_value(PARAM_INT, 'The type of the conversation (1=individual,2=group)'),
                 'membercount' => new external_value(PARAM_INT, 'Total number of conversation members'),
-                'isfavourite' => new external_value(PARAM_BOOL, 'If the user marked conversation this conversation as a favourite'),
+                'ismuted' => new external_value(PARAM_BOOL, 'If the user muted this conversation'),
+                'isfavourite' => new external_value(PARAM_BOOL, 'If the user marked this conversation as a favourite'),
                 'isread' => new external_value(PARAM_BOOL, 'If the user has read all messages in the conversation'),
                 'unreadcount' => new external_value(PARAM_INT, 'The number of unread messages in this conversation',
                     VALUE_DEFAULT, null),
index 00ea59a..df496af 100644 (file)
@@ -61,6 +61,9 @@
                 <span class="{{^isblocked}}hidden{{/isblocked}}" data-region="contact-icon-blocked">
                     {{#pix}} t/block, core, {{#str}} contactblocked, message {{/str}} {{/pix}}
                 </span>
+                <span class="{{^ismuted}}hidden{{/ismuted}}" data-region="muted-icon-container">
+                    {{#pix}} i/muted, core {{/pix}}
+                </span>
             </div>
             {{#subname}}
                 <p class="m-0 text-truncate">{{.}}</p>
index df7b4db..3d748b6 100644 (file)
@@ -60,6 +60,9 @@
                     <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
                         {{#pix}} i/star-rating, core {{/pix}}
                     </span>
+                    <span class="{{^ismuted}}hidden{{/ismuted}} ml-1 text-primary" data-region="muted-icon-container">
+                        {{#pix}} i/muted, core {{/pix}}
+                    </span>
                 </div>
                 {{#showonlinestatus}}
                     <p class="m-0 font-weight-light text-truncate">
             <a class="dropdown-item {{^isblocked}}hidden{{/isblocked}}" href="#" data-action="request-unblock">
                 {{#str}} unblockuser, core_message {{/str}}
             </a>
+            <a class="dropdown-item {{#ismuted}}hidden{{/ismuted}}" href="#" data-action="confirm-mute">
+                {{#str}} muteconversation, core_message {{/str}}
+            </a>
+            <a class="dropdown-item {{^ismuted}}hidden{{/ismuted}}" href="#" data-action="confirm-unmute">
+                {{#str}} unmuteconversation, core_message {{/str}}
+            </a>
             <a class="dropdown-item" href="#" data-action="request-delete-conversation">
                 {{#str}} deleteconversation, core_message {{/str}}
             </a>
index e2390e5..4422fcd 100644 (file)
@@ -59,6 +59,9 @@
                 <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
                     {{#pix}} i/star-rating, core {{/pix}}
                 </span>
+                <span class="{{^ismuted}}hidden{{/ismuted}} ml-1 text-primary" data-region="muted-icon-container">
+                    {{#pix}} i/muted, core {{/pix}}
+                </span>
             </div>
             {{#showonlinestatus}}
                 <p class="m-0 font-weight-light text-truncate">
index 71a68fa..152a30d 100644 (file)
@@ -59,6 +59,9 @@
                         <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
                             {{#pix}} i/star-rating, core {{/pix}}
                         </span>
+                        <span class="{{^ismuted}}hidden{{/ismuted}} ml-1 text-primary" data-region="muted-icon-container">
+                            {{#pix}} i/muted, core {{/pix}}
+                        </span>
                     </div>
                     <p class="m-0 text-truncate">{{subname}}</p>
                 </div>
                 <a class="dropdown-item {{^isfavourite}}hidden{{/isfavourite}}" href="#" data-action="confirm-unfavourite">
                     {{#str}} removefromfavourites, core_message {{/str}}
                 </a>
+                <a class="dropdown-item {{#ismuted}}hidden{{/ismuted}}" href="#" data-action="confirm-mute">
+                    {{#str}} muteconversation, core_message {{/str}}
+                </a>
+                <a class="dropdown-item {{^ismuted}}hidden{{/ismuted}}" href="#" data-action="confirm-unmute">
+                    {{#str}} unmuteconversation, core_message {{/str}}
+                </a>
             </div>
         </div>
     </div>
index f3f534b..ba5ed18 100644 (file)
@@ -1302,6 +1302,50 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($expectedmessagetext, $messages[0]->text);
     }
 
+    /**
+     * Test verifying get_conversations identifies if a conversation is muted or not.
+     */
+    public function test_get_conversations_some_muted() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $conversation1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+        testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1');
+        testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 2');
+        \core_message\api::mute_conversation($user1->id, $conversation1->id);
+
+        $conversation2 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user3->id]);
+        testhelper::send_fake_message_to_conversation($user1, $conversation2->id, 'Message 1');
+        testhelper::send_fake_message_to_conversation($user2, $conversation2->id, 'Message 2');
+
+        $conversation3 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id]);
+        \core_message\api::mute_conversation($user1->id, $conversation3->id);
+
+        $conversation4 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user3->id]);
+
+        $conversations = \core_message\api::get_conversations($user1->id);
+
+        usort($conversations, function($first, $second){
+            return $first->id > $second->id;
+        });
+
+        $conv1 = array_shift($conversations);
+        $conv2 = array_shift($conversations);
+        $conv3 = array_shift($conversations);
+        $conv4 = array_shift($conversations);
+
+        $this->assertTrue($conv1->ismuted);
+        $this->assertFalse($conv2->ismuted);
+        $this->assertTrue($conv3->ismuted);
+        $this->assertFalse($conv4->ismuted);
+    }
+
     /**
      * Tests retrieving conversations with a limit and offset to ensure pagination works correctly.
      */
@@ -4912,6 +4956,84 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(0, $DB->count_records('message_users_blocked'));
     }
 
+    /**
+     * Test muting a conversation.
+     */
+    public function test_mute_conversation() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+        $conversationid = $conversation->id;
+
+        \core_message\api::mute_conversation($user1->id, $conversationid);
+
+        $mutedconversation = $DB->get_records('message_conversation_actions');
+
+        $this->assertCount(1, $mutedconversation);
+
+        $mutedconversation = reset($mutedconversation);
+
+        $this->assertEquals($user1->id, $mutedconversation->userid);
+        $this->assertEquals($conversationid, $mutedconversation->conversationid);
+        $this->assertEquals(\core_message\api::CONVERSATION_ACTION_MUTED, $mutedconversation->action);
+    }
+
+    /**
+     * Test unmuting a conversation.
+     */
+    public function test_unmute_conversation() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+        $conversationid = $conversation->id;
+
+        \core_message\api::mute_conversation($user1->id, $conversationid);
+        \core_message\api::unmute_conversation($user1->id, $conversationid);
+
+        $this->assertEquals(0, $DB->count_records('message_conversation_actions'));
+    }
+
+    /**
+     * Test if a conversation is muted.
+     */
+    public function test_is_conversation_muted() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+        $conversationid = $conversation->id;
+
+        $this->assertFalse(\core_message\api::is_conversation_muted($user1->id, $conversationid));
+
+        \core_message\api::mute_conversation($user1->id, $conversationid);
+
+        $this->assertTrue(\core_message\api::is_conversation_muted($user1->id, $conversationid));
+    }
+
     /**
      * Test is contact check.
      */
@@ -5772,6 +5894,32 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         \core_message\api::send_message_to_conversation($user2->id, $ic1->id, 'test', FORMAT_MOODLE);
     }
 
+    /**
+     * Test the get_conversation() function with a muted conversation.
+     */
+    public function test_get_conversation_with_muted_conversation() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+
+        $conversation = \core_message\api::get_conversation($user1->id, $conversation->id);
+
+        $this->assertFalse($conversation->ismuted);
+
+        // Now, mute the conversation.
+        \core_message\api::mute_conversation($user1->id, $conversation->id);
+
+        $conversation = \core_message\api::get_conversation($user1->id, $conversation->id);
+
+        $this->assertTrue($conversation->ismuted);
+    }
+
     /**
      * Data provider for test_get_conversation_counts().
      */
index 8916e1e..41e1722 100644 (file)
@@ -37,6 +37,26 @@ require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
  */
 class behat_message extends behat_base {
 
+    /**
+     * Open the messaging UI.
+     *
+     * @Given /^I open messaging$/
+     */
+    public function i_open_messaging() {
+        // Visit home page and follow messages.
+        $this->execute("behat_general::i_am_on_homepage");
+        $this->execute("behat_general::i_click_on", [get_string('togglemessagemenu', 'core_message'), 'link']);
+    }
+
+    /**
+     * Open the messaging UI.
+     *
+     * @Given /^I open messaging information$/
+     */
+    public function i_open_messaging_information() {
+        $this->execute('behat_general::i_click_on', ["[data-action='view-group-info']", 'css_element']);
+    }
+
     /**
      * View the contact information of a user in the messages ui.
      *
@@ -67,11 +87,7 @@ class behat_message extends behat_base {
      * @param string $userfullname
      */
     public function i_select_user_in_messaging($userfullname) {
-
-        // Visit home page and follow messages.
-        $this->execute("behat_general::i_am_on_homepage");
-
-        $this->execute("behat_general::i_click_on", [get_string('togglemessagemenu', 'core_message'), 'link']);
+        $this->i_open_messaging();
 
         $this->execute('behat_general::i_click_on', [get_string('search', 'core'), 'field']);
 
@@ -100,7 +116,6 @@ class behat_message extends behat_base {
         $this->execute('behat_general::wait_until_the_page_is_ready');
     }
 
-
     /**
      * Sends a message to the specified user from the logged user. The user full name should contain the first and last names.
      *
@@ -140,4 +155,36 @@ class behat_message extends behat_base {
 
         $this->execute("behat_forms::press_button", get_string('send', 'message'));
     }
+
+    /**
+     * Navigate back in the messages ui drawer.
+     *
+     * @Given /^I go back in "(?P<parent_element_string>(?:[^"]|\\")*)" message drawer$/
+     * @param string $parentelement
+     */
+    public function i_go_back_in_message_drawer($parentelement) {
+        $this->execute('behat_general::i_click_on_in_the',
+            array(
+                'a[data-route-back]',
+                'css_element',
+                '[data-region="'.$this->escape($parentelement).'"]',
+                'css_element',
+            )
+        );
+    }
+
+    /**
+     * Select a user in the messaging UI.
+     *
+     * @Given /^I select "(?P<conversation_name_string>(?:[^"]|\\")*)" conversation in messaging$/
+     * @param string $conversationname
+     */
+    public function i_select_conversation_in_messaging($conversationname) {
+        $this->execute('behat_general::i_click_on',
+            array(
+                $this->escape($conversationname),
+                'group_message',
+            )
+        );
+    }
 }
diff --git a/message/tests/behat/group_conversation.feature b/message/tests/behat/group_conversation.feature
new file mode 100644 (file)
index 0000000..c6fce93
--- /dev/null
@@ -0,0 +1,54 @@
+@core @core_message @javascript
+Feature: Create conversations for course's groups
+  In order to manage a course group in a course
+  As a user
+  I need to be able to ensure group conversations reflect the memberships of course groups
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1        | 0        | 1         |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | student0 | Student   | 0        | student0@example.com |
+      | student1 | Student   | 1        | student1@example.com |
+      | student2 | Student   | 2        | student2@example.com |
+      | student3 | Student   | 3        | student3@example.com |
+      | student4 | Student   | 4        | student4@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role |
+      | teacher1 | C1     | editingteacher |
+      | student0 | C1     | student |
+      | student1 | C1     | student |
+      | student2 | C1     | student |
+      | student3 | C1     | student |
+      | student4 | C1     | student |
+    And the following "groups" exist:
+      | name    | course | idnumber | enablemessaging |
+      | Group 1 | C1     | G1       | 1               |
+      | Group 2 | C1     | G2       | 1               |
+      | Group 3 | C1     | G3       | 0               |
+    And the following "group members" exist:
+      | user     | group |
+      | teacher1 | G1 |
+      | student0 | G1 |
+      | student1 | G1 |
+      | student2 | G1 |
+      | student3 | G1 |
+      | teacher1 | G2 |
+      | teacher1 | G3 |
+      | student0 | G3 |
+
+  Scenario: Group conversations are restricted to members
+    Given I log in as "teacher1"
+    Then I open messaging
+    And "Group 1" "group_message" should exist
+    And "Group 2" "group_message" should exist
+    And "Group 3" "group_message" should not exist
+    And I log out
+    And I log in as "student1"
+    And I open messaging
+    And "Group 1" "group_message" should exist
+    And "Group 2" "group_message" should not exist
+    And "Group 3" "group_message" should not exist
index 92c2698..d76e1ce 100644 (file)
@@ -1022,6 +1022,168 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         core_message_external::decline_contact_request($user1->id, $user2->id);
     }
 
+    /**
+     * Test muting conversations.
+     */
+    public function test_mute_conversations() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+
+        $this->setUser($user1);
+
+        // Muting a conversation.
+        $return = core_message_external::mute_conversations($user1->id, [$conversation->id]);
+        $return = external_api::clean_returnvalue(core_message_external::mute_conversations_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        // Get list of muted conversations.
+        $mca = $DB->get_record('message_conversation_actions', []);
+
+        $this->assertEquals($user1->id, $mca->userid);
+        $this->assertEquals($conversation->id, $mca->conversationid);
+        $this->assertEquals(\core_message\api::CONVERSATION_ACTION_MUTED, $mca->action);
+
+        // Muting a conversation that is already muted.
+        $return = core_message_external::mute_conversations($user1->id, [$conversation->id]);
+        $return = external_api::clean_returnvalue(core_message_external::mute_conversations_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        $this->assertEquals(1, $DB->count_records('message_conversation_actions'));
+    }
+
+    /**
+     * Test muting a conversation with messaging disabled.
+     */
+    public function test_mute_conversations_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::mute_conversations($user1->id, [$conversation->id]);
+    }
+
+    /**
+     * Test muting a conversation with no permission.
+     */
+    public function test_mute_conversations_no_permission() {
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+
+        $this->setUser($user3);
+
+        // Ensure an exception is thrown.
+        $this->expectException('required_capability_exception');
+        core_message_external::mute_conversations($user1->id, [$conversation->id]);
+    }
+
+    /**
+     * Test unmuting conversations.
+     */
+    public function test_unmute_conversations() {
+        global $DB;
+
+        $this->resetAfterTest(true);
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+
+        $this->setUser($user1);
+
+        // Mute the conversation.
+        \core_message\api::mute_conversation($user1->id, $conversation->id);
+
+        // Unmuting a conversation.
+        $return = core_message_external::unmute_conversations($user1->id, [$conversation->id]);
+        $return = external_api::clean_returnvalue(core_message_external::unmute_conversations_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        $this->assertEquals(0, $DB->count_records('message_conversation_actions'));
+
+        // Unmuting a conversation which is already unmuted.
+        $return = core_message_external::unmute_conversations($user1->id, [$conversation->id]);
+        $return = external_api::clean_returnvalue(core_message_external::unmute_conversations_returns(), $return);
+        $this->assertEquals(array(), $return);
+
+        $this->assertEquals(0, $DB->count_records('message_conversation_actions'));
+    }
+
+    /**
+     * Test unmuting a conversation with messaging disabled.
+     */
+    public function test_unmute_conversation_messaging_disabled() {
+        global $CFG;
+
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+
+        $this->setUser($user1);
+
+        // Disable messaging.
+        $CFG->messaging = 0;
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::unmute_conversations($user1->id, [$user2->id]);
+    }
+
+    /**
+     * Test unmuting a conversation with no permission.
+     */
+    public function test_unmute_conversation_no_permission() {
+        $this->resetAfterTest();
+
+        // Create some skeleton data just so we can call the WS.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+
+        $this->setUser($user3);
+
+        // Ensure an exception is thrown.
+        $this->expectException('required_capability_exception');
+        core_message_external::unmute_conversations($user1->id, [$conversation->id]);
+    }
+
     /**
      * Test blocking a user.
      */
@@ -5654,6 +5816,55 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertNotEmpty($individualmember['contactrequests']);
     }
 
+    /**
+     * Test verifying get_conversations identifies if a conversation is muted or not.
+     */
+    public function test_get_conversations_some_muted() {
+        $this->resetAfterTest();
+
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $conversation1 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]);
+        testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1');
+        testhelper::send_fake_message_to_conversation($user2, $conversation1->id, 'Message 2');
+        \core_message\api::mute_conversation($user1->id, $conversation1->id);
+
+        $conversation2 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user3->id]);
+        testhelper::send_fake_message_to_conversation($user1, $conversation2->id, 'Message 1');
+        testhelper::send_fake_message_to_conversation($user2, $conversation2->id, 'Message 2');
+
+        $conversation3 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id]);
+        \core_message\api::mute_conversation($user1->id, $conversation3->id);
+
+        $conversation4 = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user3->id]);
+
+        $this->setUser($user1);
+        $result = core_message_external::get_conversations($user1->id);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+
+        usort($conversations, function($first, $second){
+            return $first['id'] > $second['id'];
+        });
+
+        $conv1 = array_shift($conversations);
+        $conv2 = array_shift($conversations);
+        $conv3 = array_shift($conversations);
+        $conv4 = array_shift($conversations);
+
+        $this->assertTrue($conv1['ismuted']);
+        $this->assertFalse($conv2['ismuted']);
+        $this->assertTrue($conv3['ismuted']);
+        $this->assertFalse($conv4['ismuted']);
+    }
+
     /**
      * Test returning members in a conversation with no contact requests.
      */
index 425c425..6fc2436 100644 (file)
@@ -47,7 +47,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $collection = new collection('core_message');
         $newcollection = provider::get_metadata($collection);
         $itemcollection = $newcollection->get_collection();
-        $this->assertCount(9, $itemcollection);
+        $this->assertCount(10, $itemcollection);
 
         $messagestable = array_shift($itemcollection);
         $this->assertEquals('messages', $messagestable->get_name());
@@ -58,6 +58,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $messageconversationmemberstable = array_shift($itemcollection);
         $this->assertEquals('message_conversation_members', $messageconversationmemberstable->get_name());
 
+        $messageconversationactions = array_shift($itemcollection);
+        $this->assertEquals('message_conversation_actions', $messageconversationactions->get_name());
+
         $messagecontacts = array_shift($itemcollection);
         $this->assertEquals('message_contacts', $messagecontacts->get_name());
 
@@ -1584,6 +1587,10 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         \core_message\api::set_favourite_conversation($conversation1->id, $user1->id);
         \core_message\api::set_favourite_conversation($iconversation1id, $user2->id);
 
+        // Mute some conversations.
+        \core_message\api::mute_conversation($user1->id, $conversation1->id);
+        \core_message\api::mute_conversation($user2->id, $iconversation1id);
+
         // Send some messages to the conversation.
         $m1 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 1', $now + 1);
         $m2 = testhelper::send_fake_message_to_conversation($user1, $conversation1->id, 'Message 2', $now + 2);
@@ -1646,6 +1653,15 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(transform::datetime($now + 5), $m3->timeread);
         $this->assertArrayNotHasKey('timedeleted', (array) $m3);
 
+        // Confirm the muted group conversation is correct.
+        $mutedconversations = (array) $writer->get_related_data([
+            get_string('messages', 'core_message'),
+            get_string($conversation1->itemtype, $conversation1->component),
+            get_string('privacy:export:conversationprefix', 'core_message') . $conversation1->name
+        ], 'muted');
+        $this->assertCount(2, $mutedconversations);
+        $this->assertEquals(get_string('yes'), $mutedconversations['muted']);
+
         // Confirm the favourite group conversation is correct.
         $favourite = (array) $writer->get_related_data([
             get_string('messages', 'core_message'),
@@ -1707,6 +1723,14 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals('-', $m3->timeread);
         $this->assertArrayNotHasKey('timedeleted', (array) $m3);
 
+        // Confirm the muted group conversation is correct.
+        $mutedconversations = (array) $writer->get_related_data([
+            get_string('messages', 'core_message'),
+            get_string($conversation1->itemtype, $conversation1->component),
+            $conversation1->name
+        ], 'muted');
+        $this->assertCount(0, $mutedconversations);
+
         // Confirm there are no favourite group conversation for user2.
         $favourite = (array) $writer->get_related_data([
             get_string('messages', 'core_message'),
@@ -1822,6 +1846,11 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // Mark as read one of the conversation messages.
         \core_message\api::mark_message_as_read($user1->id, $dbgm3, $now + 5);
 
+        // Mark some conversations as muted by two users.
+        \core_message\api::mute_conversation($user1->id, $iconversation1id);
+        \core_message\api::mute_conversation($user1->id, $conversation1->id);
+        \core_message\api::mute_conversation($user2->id, $conversation1->id);
+
         // There should be 2 contacts.
         $this->assertEquals(2, $DB->count_records('message_contacts'));
 
@@ -1837,6 +1866,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 4 user actions - 3 for reading the message, 1 for deleting.
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
 
+        // There should be 3 muted conversations.
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
+
         // There should be 4 conversations - 2 individual + 2 group.
         $this->assertEquals(4, $DB->count_records('message_conversations'));
 
@@ -1861,7 +1893,10 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be still 2 blocked users.
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
 
-        // There should be 5 notifications.
+        // There should be 1 muted conversation.
+        $this->assertEquals(1, $DB->count_records('message_conversation_actions'));
+
+        // There should be 3 notifications.
         $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 5 messages - 3 individual - 2 group (course2).
@@ -1992,6 +2027,11 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
 
         $dbgm3 = $DB->get_record('messages', ['id' => $gm3]);
 
+        // Mark some conversations as muted by two users.
+        \core_message\api::mute_conversation($user1->id, $iconversation1id);
+        \core_message\api::mute_conversation($user1->id, $conversation1->id);
+        \core_message\api::mute_conversation($user2->id, $conversation1->id);
+
         // Mark as read one of the conversation messages.
         \core_message\api::mark_message_as_read($user1->id, $dbgm3, $now + 5);
 
@@ -2010,6 +2050,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 4 user actions - 3 for reading the message, 1 for deleting.
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
 
+        // There should be 3 muted conversations.
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
+
         // There should be 4 conversations - 2 individual + 2 group.
         $this->assertEquals(4, $DB->count_records('message_conversations'));
 
@@ -2031,6 +2074,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
         $this->assertEquals(8, $DB->count_records('messages'));
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
         $this->assertEquals(5, $DB->count_records('notifications'));
@@ -2045,6 +2089,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
         $this->assertEquals(8, $DB->count_records('messages'));
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
         $this->assertEquals(5, $DB->count_records('notifications'));
@@ -2154,6 +2199,11 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // Mark as read one of the conversation messages.
         \core_message\api::mark_message_as_read($user1->id, $dbgm3, $now + 5);
 
+        // Mark some of the conversations as muted by two users.
+        \core_message\api::mute_conversation($user1->id, $iconversation1id);
+        \core_message\api::mute_conversation($user1->id, $conversation1->id);
+        \core_message\api::mute_conversation($user2->id, $conversation1->id);
+
         // There should be 2 contacts.
         $this->assertEquals(2, $DB->count_records('message_contacts'));
 
@@ -2169,6 +2219,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 4 user actions - 3 for reading the message, 1 for deleting.
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
 
+        // There should be 3 muted conversations.
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
+
         // There should be 4 conversations - 2 individual + 2 group.
         $this->assertEquals(4, $DB->count_records('message_conversations'));
 
@@ -2190,6 +2243,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
         $this->assertEquals(8, $DB->count_records('messages'));
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
         $this->assertEquals(5, $DB->count_records('notifications'));
@@ -2204,6 +2258,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
         $this->assertEquals(8, $DB->count_records('messages'));
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(9, $DB->count_records('message_conversation_members'));
         $this->assertEquals(5, $DB->count_records('notifications'));
@@ -2301,6 +2356,11 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // Mark as read one of the conversation messages.
         \core_message\api::mark_message_as_read($user1->id, $dbm3, $now + 5);
 
+        // Mark some of the conversations as muted by two users.
+        \core_message\api::mute_conversation($user1->id, $iconversation1id);
+        \core_message\api::mute_conversation($user1->id, $conversation1->id);
+        \core_message\api::mute_conversation($user2->id, $conversation1->id);
+
         // There should be 2 contacts.
         $this->assertEquals(2, $DB->count_records('message_contacts'));
 
@@ -2319,6 +2379,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 4 user actions - 3 for reading the message, one for deleting.
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
 
+        // There should be 3 users muting a conversation.
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
+
         // There should be 3 conversations - 2 private + 1 group.
         $this->assertEquals(3, $DB->count_records('message_conversations'));
 
@@ -2347,7 +2410,10 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be still 2 blocked users.
         $this->assertEquals(2, $DB->count_records('message_users_blocked'));
 
-        // There should be 5 notifications.
+        // There should be 2 muted conversation.
+        $this->assertEquals(2, $DB->count_records('message_conversation_actions'));
+
+        // There should be 3 notifications.
         $this->assertEquals(5, $DB->count_records('notifications'));
 
         // There should be 4 messages - 3 private + 1 group sent by user2.
@@ -2484,6 +2550,11 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // Mark as read one of the conversation messages.
         \core_message\api::mark_message_as_read($user1->id, $dbm3, $now + 5);
 
+        // Mark some of the conversations as muted by two users.
+        \core_message\api::mute_conversation($user1->id, $iconversation1id);
+        \core_message\api::mute_conversation($user1->id, $conversation1->id);
+        \core_message\api::mute_conversation($user2->id, $conversation1->id);
+
         // There should be 2 contacts.
         $this->assertEquals(2, $DB->count_records('message_contacts'));
 
@@ -2502,6 +2573,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         // There should be 4 user actions - 3 for reading the message, one for deleting.
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
 
+        // There should be 3 muted conversation.
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
+
         // There should be 3 conversations - 2 private + 2 group.
         $this->assertEquals(4, $DB->count_records('message_conversations'));
 
@@ -2529,6 +2603,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(5, $DB->count_records('notifications'));
         $this->assertEquals(6, $DB->count_records('messages'));
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(8, $DB->count_records('message_conversation_members'));
         $this->assertEquals(3, $DB->count_records('favourite'));
@@ -2545,6 +2620,7 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         $this->assertEquals(5, $DB->count_records('notifications'));
         $this->assertEquals(6, $DB->count_records('messages'));
         $this->assertEquals(4, $DB->count_records('message_user_actions'));
+        $this->assertEquals(3, $DB->count_records('message_conversation_actions'));
         $this->assertEquals(4, $DB->count_records('message_conversations'));
         $this->assertEquals(3, $DB->count_records('favourite'));
         // There should be 7 conversation members - (2 + 2) private + 3 group.
@@ -2583,6 +2659,9 @@ class core_message_privacy_provider_testcase extends \core_privacy\tests\provide
         }, $useractions);
         $this->assertNotContains($gm3, $useractions);
 
+        // There should be 1 muted conversation.
+        $this->assertEquals(1, $DB->count_records('message_conversation_actions'));
+
         // There should be still 4 conversations - 2 private + 2 group.
         $this->assertEquals(4, $DB->count_records('message_conversations'));
 
diff --git a/pix/i/muted.png b/pix/i/muted.png
new file mode 100755 (executable)
index 0000000..c0440fc
Binary files /dev/null and b/pix/i/muted.png differ
diff --git a/pix/i/muted.svg b/pix/i/muted.svg
new file mode 100755 (executable)
index 0000000..830d7e2
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><title>Artboard</title><path d="M6.72,2.22,3.94,5H.75A.75.75,0,0,0,0,5.75v4.5A.75.75,0,0,0,.75,11H3.94l2.78,2.78A.75.75,0,0,0,8,13.25V2.75a.75.75,0,0,0-1.28-.53ZM14.43,8l1.43-1.43a.5.5,0,0,0,0-.71h0l-.71-.71a.5.5,0,0,0-.71,0h0L13,6.57,11.57,5.15a.5.5,0,0,0-.71,0h0l-.71.71a.5.5,0,0,0,0,.71h0L11.57,8,10.15,9.43a.5.5,0,0,0,0,.71h0l.71.71a.5.5,0,0,0,.71,0h0L13,9.43l1.43,1.43a.5.5,0,0,0,.71,0h0l.71-.71a.5.5,0,0,0,0-.71h0Z" fill="#999"/></svg>
\ No newline at end of file
index 3b0ea73..18f4d49 100644 (file)
     position: absolute;
 }
 
-.fp-iconview .fp-filename-field:hover {
-    overflow: visible;
-    z-index: 1000;
+.fp-iconview .fp-file:focus,
+.fp-iconview .fp-file:hover {
+    // Undo truncating of text on hover.
+    .fp-filename-field {
+        overflow: visible;
+        z-index: 1000;
+    }
+    .fp-filename {
+        overflow: inherit;
+        white-space: normal;
+        text-overflow: inherit;
+    }
 }
 
 .fp-iconview .fp-filename-field .fp-filename {
index 4937913..f50e221 100644 (file)
@@ -12736,10 +12736,17 @@ body.drawer-ease {
   overflow: hidden;
   position: absolute; }
 
-.fp-iconview .fp-filename-field:hover {
+.fp-iconview .fp-file:focus .fp-filename-field,
+.fp-iconview .fp-file:hover .fp-filename-field {
   overflow: visible;
   z-index: 1000; }
 
+.fp-iconview .fp-file:focus .fp-filename,
+.fp-iconview .fp-file:hover .fp-filename {
+  overflow: inherit;
+  white-space: normal;
+  text-overflow: inherit; }
+
 .fp-iconview .fp-filename-field .fp-filename {
   background: #fff;
   padding-top: 5px;
diff --git a/theme/boost/tests/behat/group_conversation.feature b/theme/boost/tests/behat/group_conversation.feature
new file mode 100644 (file)
index 0000000..8738b9b
--- /dev/null
@@ -0,0 +1,90 @@
+@javascript @theme_boost
+Feature: Create conversations for course's groups in Boost theme
+  In order to manage a course group in a course
+  As a user
+  I need to be able to ensure group conversations reflect the memberships of course groups
+
+  Background:
+    Given the following "courses" exist:
+      | fullname | shortname | category | groupmode |
+      | Course 1 | C1        | 0        | 1         |
+    And the following "users" exist:
+      | username | firstname | lastname | email                |
+      | teacher1 | Teacher   | 1        | teacher1@example.com |
+      | student0 | Student   | 0        | student0@example.com |
+      | student1 | Student   | 1        | student1@example.com |
+      | student2 | Student   | 2        | student2@example.com |
+      | student3 | Student   | 3        | student3@example.com |
+      | student4 | Student   | 4        | student4@example.com |
+    And the following "course enrolments" exist:
+      | user     | course | role |
+      | teacher1 | C1     | editingteacher |
+      | student0 | C1     | student |
+      | student1 | C1     | student |
+      | student2 | C1     | student |
+      | student3 | C1     | student |
+      | student4 | C1     | student |
+    And the following "groups" exist:
+      | name    | course | idnumber | enablemessaging |
+      | Group 1 | C1     | G1       | 1               |
+      | Group 2 | C1     | G2       | 1               |
+      | Group 3 | C1     | G3       | 0               |
+    And the following "group members" exist:
+      | user     | group |
+      | teacher1 | G1 |
+      | student0 | G1 |
+      | student1 | G1 |
+      | student2 | G1 |
+      | student3 | G1 |
+      | teacher1 | G2 |
+      | teacher1 | G3 |
+      | student0 | G3 |
+
+  Scenario: View group conversation's participants numbers
+    Given I log in as "teacher1"
+    Then I open messaging
+    And I select "Group 1" conversation in messaging
+    And "5 participants" "group_message_header" should exist
+    And I go back in "view-conversation" message drawer
+    And I select "Group 2" conversation in messaging
+    And "1 participants" "group_message_header" should exist
+
+  Scenario: View group conversation's participants list
+    Given I log in as "teacher1"
+    Then I open messaging
+    # Check Group 1 participants list.
+    And I select "Group 1" conversation in messaging
+    And I open messaging information
+    And "Teacher 1" "group_message_member" should not exist
+    And "Student 0" "group_message_member" should exist
+    And "Student 1" "group_message_member" should exist
+    And "Student 2" "group_message_member" should exist
+    And "Student 3" "group_message_member" should exist
+    And "Student 4" "group_message_member" should not exist
+    And I go back in "group-info-content-container" message drawer
+    And I go back in "view-conversation" message drawer
+    # Check Group 2 participants list.
+    And I select "Group 2" conversation in messaging
+    And I open messaging information
+    And "Teacher 1" "group_message_member" should not exist
+    And "No participants" "group_message_member" should exist
+    And "Student 4" "group_message_member" should not exist
+
+  Scenario: Check group conversation members are synced when a new group member is added
+    Given I log in as "teacher1"
+    Then I am on "Course 1" course homepage
+    And I navigate to "Users > Groups" in current page administration
+    And I add "Student 4 (student4@example.com)" user to "Group 1" group members
+    And I add "Student 4 (student4@example.com)" user to "Group 2" group members
+    And I open messaging
+    And I select "Group 1" conversation in messaging
+    And "6 participants" "group_message_header" should exist
+    And I open messaging information
+    And "Student 4" "group_message_member" should exist
+    And I go back in "group-info-content-container" message drawer
+    And I go back in "view-conversation" message drawer
+    And I select "Group 2" conversation in messaging
+    And "2 participants" "group_message_header" should exist
+    And I open messaging information
+    And "No participants" "group_message_member" should not exist
+    And "Student 4" "group_message_member" should exist
index 4591b25..1bf70aa 100644 (file)
     overflow: hidden;
     position: absolute;
 }
-.fp-iconview .fp-filename-field:hover {
-    overflow: visible;
-    z-index: 1000;
+.fp-iconview .fp-file:focus,
+.fp-iconview .fp-file:hover {
+    // Undo truncating of text on hover of focus.
+    .fp-filename-field {
+        overflow: visible;
+        z-index: 1000;
+    }
+    .fp-filename {
+        overflow: inherit;
+        white-space: normal;
+        text-overflow: unset;
+    }
 }
 .fp-iconview .fp-filename-field .fp-filename {
     background: #fff;
index 5eeb24b..5c57a11 100644 (file)
@@ -7371,10 +7371,17 @@ span.editinstructions {
   overflow: hidden;
   position: absolute;
 }
-.fp-iconview .fp-filename-field:hover {
+.fp-iconview .fp-file:focus .fp-filename-field,
+.fp-iconview .fp-file:hover .fp-filename-field {
   overflow: visible;
   z-index: 1000;
 }
+.fp-iconview .fp-file:focus .fp-filename,
+.fp-iconview .fp-file:hover .fp-filename {
+  overflow: inherit;
+  white-space: normal;
+  text-overflow: unset;
+}
 .fp-iconview .fp-filename-field .fp-filename {
   background: #fff;
   padding-top: 5px;
index 1b35f9f..9ede242 100644 (file)
@@ -65,6 +65,9 @@
                     <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
                         {{#pix}} i/star, core {{/pix}}
                     </span>
+                    <span class="{{^ismuted}}hidden{{/ismuted}} ml-1 text-primary" data-region="muted-icon-container">
+                        {{#pix}} i/muted, core {{/pix}}
+                    </span>
                 </div>
                 {{#showonlinestatus}}
                     <p class="m-0 font-weight-light text-truncate">
                     {{#str}} unblockuser, core_message {{/str}}
                 </a>
             </li>
+            <li class="{{#ismuted}}hidden{{/ismuted}}" data-action="confirm-mute">
+                <a class="dropdown-item" href="#">
+                    {{#str}} muteconversation, core_message {{/str}}
+                </a>
+            </li>
+            <li class="{{^ismuted}}hidden{{/ismuted}}" data-action="confirm-unmute">
+                <a class="dropdown-item" href="#">
+                    {{#str}} unmuteconversation, core_message {{/str}}
+                </a>
+            </li>
             <li data-action="request-delete-conversation">
                 <a class="dropdown-item" href="#">
                     {{#str}} deleteconversation, core_message {{/str}}
index 5fb4fdc..2e7ece4 100644 (file)
@@ -64,6 +64,9 @@
                 <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
                     {{#pix}} i/star, core {{/pix}}
                 </span>
+                <span class="{{^ismuted}}hidden{{/ismuted}} ml-1 text-primary" data-region="muted-icon-container">
+                    {{#pix}} i/muted, core {{/pix}}
+                </span>
             </div>
             {{#showonlinestatus}}
                 <p class="m-0 font-weight-light text-truncate">
index e9d8a8e..b6613d4 100644 (file)
@@ -63,6 +63,9 @@
                         <span class="{{^isfavourite}}hidden{{/isfavourite}} ml-1 text-primary" data-region="favourite-icon-container">
                             {{#pix}} i/star, core {{/pix}}
                         </span>
+                        <span class="{{^ismuted}}hidden{{/ismuted}} ml-1 text-primary" data-region="muted-icon-container">
+                            {{#pix}} i/muted, core {{/pix}}
+                        </span>
                     </div>
                     <p class="m-0 text-truncate">{{subname}}</p>
                 </div>
                         {{#str}} groupinfo, core_message {{/str}}
                     </a>
                 </li>
+                <li class="{{#ismuted}}hidden{{/ismuted}}" data-action="confirm-mute">
+                    <a class="dropdown-item" href="#">
+                        {{#str}} muteconversation, core_message {{/str}}
+                    </a>
+                </li>
+                <li class="{{^ismuted}}hidden{{/ismuted}}" data-action="confirm-unmute">
+                    <a class="dropdown-item" href="#">
+                        {{#str}} unmuteconversation, core_message {{/str}}
+                    </a>
+                </li>
                 <li class="{{#isfavourite}}hidden{{/isfavourite}}" data-action="confirm-favourite">
                     <a class="dropdown-item" href="#">
                         {{#str}} addtofavourites, core_message {{/str}}
index b65c517..5656a45 100644 (file)
@@ -29,7 +29,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$version  = 2019030700.01;              // YYYYMMDD      = weekly release date of this DEV branch.
+$version  = 2019030800.01;              // YYYYMMDD      = weekly release date of this DEV branch.
                                         //         RR    = release increments - 00 in DEV branches.
                                         //           .XX = incremental changes.