| 1 | efrain | 1 | // This file is part of Moodle - http://moodle.org/
 | 
        
           |  |  | 2 | //
 | 
        
           |  |  | 3 | // Moodle is free software: you can redistribute it and/or modify
 | 
        
           |  |  | 4 | // it under the terms of the GNU General Public License as published by
 | 
        
           |  |  | 5 | // the Free Software Foundation, either version 3 of the License, or
 | 
        
           |  |  | 6 | // (at your option) any later version.
 | 
        
           |  |  | 7 | //
 | 
        
           |  |  | 8 | // Moodle is distributed in the hope that it will be useful,
 | 
        
           |  |  | 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
        
           |  |  | 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
        
           |  |  | 11 | // GNU General Public License for more details.
 | 
        
           |  |  | 12 | //
 | 
        
           |  |  | 13 | // You should have received a copy of the GNU General Public License
 | 
        
           |  |  | 14 | // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 | 
        
           |  |  | 15 |   | 
        
           |  |  | 16 | /**
 | 
        
           |  |  | 17 |  * Controls the overview page of the message drawer.
 | 
        
           |  |  | 18 |  *
 | 
        
           |  |  | 19 |  * @module     core_message/message_drawer_view_overview
 | 
        
           |  |  | 20 |  * @copyright  2018 Ryan Wyllie <ryan@moodle.com>
 | 
        
           |  |  | 21 |  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 22 |  */
 | 
        
           |  |  | 23 | define(
 | 
        
           |  |  | 24 | [
 | 
        
           |  |  | 25 |     'jquery',
 | 
        
           |  |  | 26 |     'core/key_codes',
 | 
        
           |  |  | 27 |     'core/pubsub',
 | 
        
           |  |  | 28 |     'core/str',
 | 
        
           |  |  | 29 |     'core_message/message_drawer_router',
 | 
        
           |  |  | 30 |     'core_message/message_drawer_routes',
 | 
        
           |  |  | 31 |     'core_message/message_drawer_events',
 | 
        
           |  |  | 32 |     'core_message/message_drawer_view_overview_section',
 | 
        
           |  |  | 33 |     'core_message/message_repository',
 | 
        
           |  |  | 34 |     'core_message/message_drawer_view_conversation_constants'
 | 
        
           |  |  | 35 | ],
 | 
        
           |  |  | 36 | function(
 | 
        
           |  |  | 37 |     $,
 | 
        
           |  |  | 38 |     KeyCodes,
 | 
        
           |  |  | 39 |     PubSub,
 | 
        
           |  |  | 40 |     Str,
 | 
        
           |  |  | 41 |     Router,
 | 
        
           |  |  | 42 |     Routes,
 | 
        
           |  |  | 43 |     MessageDrawerEvents,
 | 
        
           |  |  | 44 |     Section,
 | 
        
           |  |  | 45 |     MessageRepository,
 | 
        
           |  |  | 46 |     Constants
 | 
        
           |  |  | 47 | ) {
 | 
        
           |  |  | 48 |   | 
        
           |  |  | 49 |     var SELECTORS = {
 | 
        
           |  |  | 50 |         CONTACT_REQUEST_COUNT: '[data-region="contact-request-count"]',
 | 
        
           |  |  | 51 |         FAVOURITES: '[data-region="view-overview-favourites"]',
 | 
        
           |  |  | 52 |         GROUP_MESSAGES: '[data-region="view-overview-group-messages"]',
 | 
        
           |  |  | 53 |         MESSAGES: '[data-region="view-overview-messages"]',
 | 
        
           |  |  | 54 |         SEARCH_INPUT: '[data-region="view-overview-search-input"]',
 | 
        
           | 1441 | ariadna | 55 |         SECTION_TOGGLE_BUTTON: '[data-bs-toggle]'
 | 
        
           | 1 | efrain | 56 |     };
 | 
        
           |  |  | 57 |   | 
        
           |  |  | 58 |     // Categories displayed in the message drawer. Some methods (such as filterCountsByType) are expecting their value
 | 
        
           |  |  | 59 |     // will be the same as the defined in the CONVERSATION_TYPES, except for the favourite.
 | 
        
           |  |  | 60 |     var OVERVIEW_SECTION_TYPES = {
 | 
        
           |  |  | 61 |         PRIVATE: [Constants.CONVERSATION_TYPES.PRIVATE, Constants.CONVERSATION_TYPES.SELF],
 | 
        
           |  |  | 62 |         PUBLIC: [Constants.CONVERSATION_TYPES.PUBLIC],
 | 
        
           |  |  | 63 |         FAVOURITE: null
 | 
        
           |  |  | 64 |     };
 | 
        
           |  |  | 65 |   | 
        
           |  |  | 66 |     var loadAllCountsPromise = null;
 | 
        
           |  |  | 67 |   | 
        
           |  |  | 68 |     /**
 | 
        
           |  |  | 69 |      * Load the total and unread conversation counts from the server for this user. This function
 | 
        
           |  |  | 70 |      * returns a jQuery promise that will be resolved with the counts.
 | 
        
           |  |  | 71 |      *
 | 
        
           |  |  | 72 |      * The request is only sent once per page load and will be cached for subsequent
 | 
        
           |  |  | 73 |      * calls to this function.
 | 
        
           |  |  | 74 |      *
 | 
        
           |  |  | 75 |      * @param {Number} loggedInUserId The logged in user's id
 | 
        
           |  |  | 76 |      * @return {Object} jQuery promise
 | 
        
           |  |  | 77 |      */
 | 
        
           |  |  | 78 |     var loadAllCounts = function(loggedInUserId) {
 | 
        
           |  |  | 79 |         if (loadAllCountsPromise === null) {
 | 
        
           |  |  | 80 |             loadAllCountsPromise = MessageRepository.getAllConversationCounts(loggedInUserId);
 | 
        
           |  |  | 81 |         }
 | 
        
           |  |  | 82 |   | 
        
           |  |  | 83 |         return loadAllCountsPromise;
 | 
        
           |  |  | 84 |     };
 | 
        
           |  |  | 85 |   | 
        
           |  |  | 86 |     /**
 | 
        
           |  |  | 87 |      * Filter a set of counts to return only the count for the given type.
 | 
        
           |  |  | 88 |      *
 | 
        
           |  |  | 89 |      * This is used on the result returned by the loadAllCounts function.
 | 
        
           |  |  | 90 |      *
 | 
        
           |  |  | 91 |      * @param {Object} counts Conversation counts indexed by conversation type.
 | 
        
           |  |  | 92 |      * @param {Array|null} types The conversation types handlded by this section (null for all conversation types).
 | 
        
           |  |  | 93 |      * @param {bool} includeFavourites If this section includes favourites
 | 
        
           |  |  | 94 |      * @return {Number}
 | 
        
           |  |  | 95 |      */
 | 
        
           |  |  | 96 |     var filterCountsByTypes = function(counts, types, includeFavourites) {
 | 
        
           |  |  | 97 |         var total = 0;
 | 
        
           |  |  | 98 |   | 
        
           |  |  | 99 |         if (types && types.length) {
 | 
        
           |  |  | 100 |             total = types.reduce(function(carry, type) {
 | 
        
           |  |  | 101 |                 return carry + counts.types[type];
 | 
        
           |  |  | 102 |             }, total);
 | 
        
           |  |  | 103 |         }
 | 
        
           |  |  | 104 |   | 
        
           |  |  | 105 |         if (includeFavourites) {
 | 
        
           |  |  | 106 |             total += counts.favourites;
 | 
        
           |  |  | 107 |         }
 | 
        
           |  |  | 108 |   | 
        
           |  |  | 109 |         return total;
 | 
        
           |  |  | 110 |     };
 | 
        
           |  |  | 111 |   | 
        
           |  |  | 112 |     /**
 | 
        
           |  |  | 113 |      * Opens one of the sections based on whether the section has unread conversations
 | 
        
           |  |  | 114 |      * or any conversations
 | 
        
           |  |  | 115 |      *
 | 
        
           |  |  | 116 |      * Default section priority is favourites, groups, then messages. A section can increase
 | 
        
           |  |  | 117 |      * in priority if it has conversations in it. It can increase even further if it has
 | 
        
           |  |  | 118 |      * unread conversations.
 | 
        
           |  |  | 119 |      *
 | 
        
           |  |  | 120 |      * @param {Array} sections List of section roots, total counts, and unread counts.
 | 
        
           |  |  | 121 |      */
 | 
        
           |  |  | 122 |     var openSection = function(sections) {
 | 
        
           |  |  | 123 |         var isAlreadyOpen = sections.some(function(section) {
 | 
        
           |  |  | 124 |             var sectionRoot = section[0];
 | 
        
           |  |  | 125 |             return Section.isVisible(sectionRoot);
 | 
        
           |  |  | 126 |         });
 | 
        
           |  |  | 127 |   | 
        
           |  |  | 128 |         if (isAlreadyOpen) {
 | 
        
           |  |  | 129 |             // The user has already opened a section so there is nothing to do.
 | 
        
           |  |  | 130 |             return;
 | 
        
           |  |  | 131 |         }
 | 
        
           |  |  | 132 |   | 
        
           |  |  | 133 |         // Order the sections so that sections with unread conversations are prioritised
 | 
        
           |  |  | 134 |         // over sections without and sections with total conversations are prioritised
 | 
        
           |  |  | 135 |         // over sections without.
 | 
        
           |  |  | 136 |         sections.sort(function(a, b) {
 | 
        
           |  |  | 137 |             var aTotal = a[1];
 | 
        
           |  |  | 138 |             var aUnread = a[2];
 | 
        
           |  |  | 139 |             var bTotal = b[1];
 | 
        
           |  |  | 140 |             var bUnread = b[2];
 | 
        
           |  |  | 141 |   | 
        
           |  |  | 142 |             if (aUnread > 0 && bUnread == 0) {
 | 
        
           |  |  | 143 |                 return -1;
 | 
        
           |  |  | 144 |             } else if (aUnread == 0 && bUnread > 0) {
 | 
        
           |  |  | 145 |                 return 1;
 | 
        
           |  |  | 146 |             } else if (aTotal > 0 && bTotal == 0) {
 | 
        
           |  |  | 147 |                 return -1;
 | 
        
           |  |  | 148 |             } else if (aTotal == 0 && bTotal > 0) {
 | 
        
           |  |  | 149 |                 return 1;
 | 
        
           |  |  | 150 |             } else {
 | 
        
           |  |  | 151 |                 return 0;
 | 
        
           |  |  | 152 |             }
 | 
        
           |  |  | 153 |         });
 | 
        
           |  |  | 154 |   | 
        
           |  |  | 155 |         // Get the root of the first section after sorting.
 | 
        
           |  |  | 156 |         var sectionRoot = sections[0][0];
 | 
        
           |  |  | 157 |         var button = sectionRoot.find(SELECTORS.SECTION_TOGGLE_BUTTON);
 | 
        
           |  |  | 158 |         // Click it to expand it.
 | 
        
           |  |  | 159 |         button.click();
 | 
        
           |  |  | 160 |     };
 | 
        
           |  |  | 161 |   | 
        
           |  |  | 162 |     /**
 | 
        
           |  |  | 163 |      * Get the search input text element.
 | 
        
           |  |  | 164 |      *
 | 
        
           |  |  | 165 |      * @param  {Object} header Overview header container element.
 | 
        
           |  |  | 166 |      * @return {Object} The search input element.
 | 
        
           |  |  | 167 |      */
 | 
        
           |  |  | 168 |     var getSearchInput = function(header) {
 | 
        
           |  |  | 169 |         return header.find(SELECTORS.SEARCH_INPUT);
 | 
        
           |  |  | 170 |     };
 | 
        
           |  |  | 171 |   | 
        
           |  |  | 172 |     /**
 | 
        
           |  |  | 173 |      * Get the logged in user id.
 | 
        
           |  |  | 174 |      *
 | 
        
           |  |  | 175 |      * @param {Object} body Overview body container element.
 | 
        
           |  |  | 176 |      * @return {String} Logged in user id.
 | 
        
           |  |  | 177 |      */
 | 
        
           |  |  | 178 |     var getLoggedInUserId = function(body) {
 | 
        
           |  |  | 179 |         return body.attr('data-user-id');
 | 
        
           |  |  | 180 |     };
 | 
        
           |  |  | 181 |   | 
        
           |  |  | 182 |     /**
 | 
        
           |  |  | 183 |      * Decrement the contact request count. If the count is zero or below then
 | 
        
           |  |  | 184 |      * hide the count.
 | 
        
           |  |  | 185 |      *
 | 
        
           |  |  | 186 |      * @param {Object} header Conversation header container element.
 | 
        
           |  |  | 187 |      * @return {Function} A function to handle decrementing the count.
 | 
        
           |  |  | 188 |      */
 | 
        
           |  |  | 189 |     var decrementContactRequestCount = function(header) {
 | 
        
           |  |  | 190 |         return function() {
 | 
        
           |  |  | 191 |             var countContainer = header.find(SELECTORS.CONTACT_REQUEST_COUNT);
 | 
        
           |  |  | 192 |             var count = parseInt(countContainer.text(), 10);
 | 
        
           |  |  | 193 |             count = isNaN(count) ? 0 : count - 1;
 | 
        
           |  |  | 194 |   | 
        
           |  |  | 195 |             if (count <= 0) {
 | 
        
           |  |  | 196 |                 countContainer.addClass('hidden');
 | 
        
           |  |  | 197 |             } else {
 | 
        
           |  |  | 198 |                 countContainer.text(count);
 | 
        
           |  |  | 199 |             }
 | 
        
           |  |  | 200 |         };
 | 
        
           |  |  | 201 |     };
 | 
        
           |  |  | 202 |   | 
        
           |  |  | 203 |     /**
 | 
        
           |  |  | 204 |      * Listen to, and handle event in the overview header.
 | 
        
           |  |  | 205 |      *
 | 
        
           |  |  | 206 |      * @param {String} namespace Unique identifier for the Routes
 | 
        
           |  |  | 207 |      * @param {Object} header Conversation header container element.
 | 
        
           |  |  | 208 |      */
 | 
        
           |  |  | 209 |     var registerEventListeners = function(namespace, header) {
 | 
        
           |  |  | 210 |         var searchInput = getSearchInput(header);
 | 
        
           |  |  | 211 |         var ignoredKeys = [KeyCodes.tab, KeyCodes.shift, KeyCodes.ctrl, KeyCodes.alt];
 | 
        
           |  |  | 212 |   | 
        
           |  |  | 213 |         searchInput.on('click', function() {
 | 
        
           |  |  | 214 |             Router.go(namespace, Routes.VIEW_SEARCH);
 | 
        
           |  |  | 215 |         });
 | 
        
           |  |  | 216 |         searchInput.on('keydown', function(e) {
 | 
        
           |  |  | 217 |             if (ignoredKeys.indexOf(e.keyCode) < 0 && e.key != 'Meta') {
 | 
        
           |  |  | 218 |                 Router.go(namespace, Routes.VIEW_SEARCH);
 | 
        
           |  |  | 219 |             }
 | 
        
           |  |  | 220 |         });
 | 
        
           |  |  | 221 |   | 
        
           |  |  | 222 |         PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, decrementContactRequestCount(header));
 | 
        
           |  |  | 223 |         PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, decrementContactRequestCount(header));
 | 
        
           |  |  | 224 |     };
 | 
        
           |  |  | 225 |   | 
        
           |  |  | 226 |     /**
 | 
        
           |  |  | 227 |      * Setup the overview page.
 | 
        
           |  |  | 228 |      *
 | 
        
           |  |  | 229 |      * @param {String} namespace Unique identifier for the Routes
 | 
        
           |  |  | 230 |      * @param {Object} header Overview header container element.
 | 
        
           |  |  | 231 |      * @param {Object} body Overview body container element.
 | 
        
           |  |  | 232 |      * @return {Object} jQuery promise
 | 
        
           |  |  | 233 |      */
 | 
        
           |  |  | 234 |     var show = function(namespace, header, body) {
 | 
        
           |  |  | 235 |         if (!header.attr('data-init')) {
 | 
        
           |  |  | 236 |             registerEventListeners(namespace, header);
 | 
        
           |  |  | 237 |             header.attr('data-init', true);
 | 
        
           |  |  | 238 |         }
 | 
        
           |  |  | 239 |         var fromPanel = header.attr('data-in-panel') ? 'frompanel' : null;
 | 
        
           |  |  | 240 |   | 
        
           |  |  | 241 |         getSearchInput(header).val('');
 | 
        
           |  |  | 242 |         var loggedInUserId = getLoggedInUserId(body);
 | 
        
           |  |  | 243 |         var allCounts = loadAllCounts(loggedInUserId);
 | 
        
           |  |  | 244 |   | 
        
           |  |  | 245 |         var sections = [
 | 
        
           |  |  | 246 |             // Favourite conversations section.
 | 
        
           |  |  | 247 |             [body.find(SELECTORS.FAVOURITES), OVERVIEW_SECTION_TYPES.FAVOURITE, true],
 | 
        
           |  |  | 248 |             // Group conversations section.
 | 
        
           |  |  | 249 |             [body.find(SELECTORS.GROUP_MESSAGES), OVERVIEW_SECTION_TYPES.PUBLIC, false],
 | 
        
           |  |  | 250 |             // Private conversations section.
 | 
        
           |  |  | 251 |             [body.find(SELECTORS.MESSAGES), OVERVIEW_SECTION_TYPES.PRIVATE, false]
 | 
        
           |  |  | 252 |         ];
 | 
        
           |  |  | 253 |   | 
        
           |  |  | 254 |         sections.forEach(function(args) {
 | 
        
           |  |  | 255 |             var sectionRoot = args[0];
 | 
        
           |  |  | 256 |             var sectionTypes = args[1];
 | 
        
           |  |  | 257 |             var includeFavourites = args[2];
 | 
        
           |  |  | 258 |             var totalCountPromise = allCounts.then(function(result) {
 | 
        
           |  |  | 259 |                 return filterCountsByTypes(result.total, sectionTypes, includeFavourites);
 | 
        
           |  |  | 260 |             });
 | 
        
           |  |  | 261 |             var unreadCountPromise = allCounts.then(function(result) {
 | 
        
           |  |  | 262 |                 return filterCountsByTypes(result.unread, sectionTypes, includeFavourites);
 | 
        
           |  |  | 263 |             });
 | 
        
           |  |  | 264 |   | 
        
           |  |  | 265 |             Section.show(namespace, null, sectionRoot, null, sectionTypes, includeFavourites,
 | 
        
           |  |  | 266 |                 totalCountPromise, unreadCountPromise, fromPanel);
 | 
        
           |  |  | 267 |         });
 | 
        
           |  |  | 268 |   | 
        
           |  |  | 269 |         return allCounts.then(function(result) {
 | 
        
           |  |  | 270 |                 var sectionParams = sections.map(function(section) {
 | 
        
           |  |  | 271 |                     var sectionRoot = section[0];
 | 
        
           |  |  | 272 |                     var sectionTypes = section[1];
 | 
        
           |  |  | 273 |                     var includeFavourites = section[2];
 | 
        
           |  |  | 274 |                     var totalCount = filterCountsByTypes(result.total, sectionTypes, includeFavourites);
 | 
        
           |  |  | 275 |                     var unreadCount = filterCountsByTypes(result.unread, sectionTypes, includeFavourites);
 | 
        
           |  |  | 276 |   | 
        
           |  |  | 277 |                     return [sectionRoot, totalCount, unreadCount];
 | 
        
           |  |  | 278 |                 });
 | 
        
           |  |  | 279 |   | 
        
           |  |  | 280 |                 // Open up one of the sections for the user.
 | 
        
           |  |  | 281 |                 return openSection(sectionParams);
 | 
        
           |  |  | 282 |             });
 | 
        
           |  |  | 283 |     };
 | 
        
           |  |  | 284 |   | 
        
           |  |  | 285 |     /**
 | 
        
           |  |  | 286 |      * String describing this page used for aria-labels.
 | 
        
           |  |  | 287 |      *
 | 
        
           |  |  | 288 |      * @return {Object} jQuery promise
 | 
        
           |  |  | 289 |      */
 | 
        
           |  |  | 290 |     var description = function() {
 | 
        
           |  |  | 291 |         return Str.get_string('messagedrawerviewoverview', 'core_message');
 | 
        
           |  |  | 292 |     };
 | 
        
           |  |  | 293 |   | 
        
           |  |  | 294 |     return {
 | 
        
           |  |  | 295 |         show: show,
 | 
        
           |  |  | 296 |         description: description
 | 
        
           |  |  | 297 |     };
 | 
        
           |  |  | 298 | });
 |