d126748cb62770a9fa7fb1e05c3c00fe9ae81cd5
[moodle.git] / message / amd / src / message_drawer_view_overview.js
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/>.
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 ) {
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"]',
55         SECTION_TOGGLE_BUTTON: '[data-toggle]'
56     };
58     var loadAllCountsPromise = null;
60     /**
61      * Load the total and unread conversation counts from the server for this user. This function
62      * returns a jQuery promise that will be resolved with the counts.
63      *
64      * The request is only sent once per page load and will be cached for subsequent
65      * calls to this function.
66      *
67      * @param {Number} loggedInUserId The logged in user's id
68      * @return {Object} jQuery promise
69      */
70     var loadAllCounts = function(loggedInUserId) {
71         if (loadAllCountsPromise === null) {
72             loadAllCountsPromise = MessageRepository.getAllConversationCounts(loggedInUserId);
73         }
75         return loadAllCountsPromise;
76     };
78     /**
79      * Filter a set of counts to return only the count for the given type.
80      *
81      * This is used on the result returned by the loadAllCounts function.
82      *
83      * @param {Object} counts Conversation counts indexed by conversation type.
84      * @param {String|null} type The conversation type (null for favourites only).
85      * @return {Number}
86      */
87     var filterCountsByType = function(counts, type) {
88         var total = 0;
89         if (type === Constants.CONVERSATION_CATEGORY_TYPES.PRIVATE && counts.types[Constants.CONVERSATION_TYPES.SELF]) {
90             // As private and self conversations are displayed together, we need to add the counts for the self-conversations
91             // to the private ones, when there is any self-conversation.
92             total = counts.types[Constants.CONVERSATION_TYPES.PRIVATE] + counts.types[Constants.CONVERSATION_TYPES.SELF];
93         } else {
94             total = type === Constants.CONVERSATION_CATEGORY_TYPES.FAVOURITE ? counts.favourites : counts.types[type];
95         }
96         return total;
97     };
99     /**
100      * Opens one of the sections based on whether the section has unread conversations
101      * or any conversations
102      *
103      * Default section priority is favourites, groups, then messages. A section can increase
104      * in priority if it has conversations in it. It can increase even further if it has
105      * unread conversations.
106      *
107      * @param {Array} sections List of section roots, total counts, and unread counts.
108      */
109     var openSection = function(sections) {
110         var isAlreadyOpen = sections.some(function(section) {
111             var sectionRoot = section[0];
112             return Section.isVisible(sectionRoot);
113         });
115         if (isAlreadyOpen) {
116             // The user has already opened a section so there is nothing to do.
117             return;
118         }
120         // Order the sections so that sections with unread conversations are prioritised
121         // over sections without and sections with total conversations are prioritised
122         // over sections without.
123         sections.sort(function(a, b) {
124             var aTotal = a[1];
125             var aUnread = a[2];
126             var bTotal = b[1];
127             var bUnread = b[2];
129             if (aUnread > 0 && bUnread == 0) {
130                 return -1;
131             } else if (aUnread == 0 && bUnread > 0) {
132                 return 1;
133             } else if (aTotal > 0 && bTotal == 0) {
134                 return -1;
135             } else if (aTotal == 0 && bTotal > 0) {
136                 return 1;
137             } else {
138                 return 0;
139             }
140         });
142         // Get the root of the first section after sorting.
143         var sectionRoot = sections[0][0];
144         var button = sectionRoot.find(SELECTORS.SECTION_TOGGLE_BUTTON);
145         // Click it to expand it.
146         button.click();
147     };
149     /**
150      * Get the search input text element.
151      *
152      * @param  {Object} header Overview header container element.
153      * @return {Object} The search input element.
154      */
155     var getSearchInput = function(header) {
156         return header.find(SELECTORS.SEARCH_INPUT);
157     };
159     /**
160      * Get the logged in user id.
161      *
162      * @param {Object} body Overview body container element.
163      * @return {String} Logged in user id.
164      */
165     var getLoggedInUserId = function(body) {
166         return body.attr('data-user-id');
167     };
169     /**
170      * Decrement the contact request count. If the count is zero or below then
171      * hide the count.
172      *
173      * @param {Object} header Conversation header container element.
174      * @return {Function} A function to handle decrementing the count.
175      */
176     var decrementContactRequestCount = function(header) {
177         return function() {
178             var countContainer = header.find(SELECTORS.CONTACT_REQUEST_COUNT);
179             var count = parseInt(countContainer.text(), 10);
180             count = isNaN(count) ? 0 : count - 1;
182             if (count <= 0) {
183                 countContainer.addClass('hidden');
184             } else {
185                 countContainer.text(count);
186             }
187         };
188     };
190     /**
191      * Listen to, and handle event in the overview header.
192      *
193      * @param {String} namespace Unique identifier for the Routes
194      * @param {Object} header Conversation header container element.
195      */
196     var registerEventListeners = function(namespace, header) {
197         var searchInput = getSearchInput(header);
198         var ignoredKeys = [KeyCodes.tab, KeyCodes.shift, KeyCodes.ctrl, KeyCodes.alt];
200         searchInput.on('click', function() {
201             Router.go(namespace, Routes.VIEW_SEARCH);
202         });
203         searchInput.on('keydown', function(e) {
204             if (ignoredKeys.indexOf(e.keyCode) < 0 && e.key != 'Meta') {
205                 Router.go(namespace, Routes.VIEW_SEARCH);
206             }
207         });
209         PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, decrementContactRequestCount(header));
210         PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, decrementContactRequestCount(header));
211     };
213     /**
214      * Setup the overview page.
215      *
216      * @param {String} namespace Unique identifier for the Routes
217      * @param {Object} header Overview header container element.
218      * @param {Object} body Overview body container element.
219      * @return {Object} jQuery promise
220      */
221     var show = function(namespace, header, body) {
222         if (!header.attr('data-init')) {
223             registerEventListeners(namespace, header);
224             header.attr('data-init', true);
225         }
227         getSearchInput(header).val('');
228         var loggedInUserId = getLoggedInUserId(body);
229         var allCounts = loadAllCounts(loggedInUserId);
231         var sections = [
232             // Favourite conversations section.
233             [body.find(SELECTORS.FAVOURITES), Constants.CONVERSATION_CATEGORY_TYPES.FAVOURITE, true],
234             // Group conversations section.
235             [body.find(SELECTORS.GROUP_MESSAGES), Constants.CONVERSATION_CATEGORY_TYPES.PUBLIC, false],
236             // Private conversations section.
237             [body.find(SELECTORS.MESSAGES), Constants.CONVERSATION_CATEGORY_TYPES.PRIVATE, false]
238         ];
240         sections.forEach(function(args) {
241             var sectionRoot = args[0];
242             var sectionType = args[1];
243             var includeFavourites = args[2];
244             var totalCountPromise = allCounts.then(function(result) {
245                 return filterCountsByType(result.total, sectionType);
246             });
247             var unreadCountPromise = allCounts.then(function(result) {
248                 return filterCountsByType(result.unread, sectionType);
249             });
251             Section.show(namespace, null, sectionRoot, null, sectionType, includeFavourites,
252                 totalCountPromise, unreadCountPromise);
253         });
255         return allCounts.then(function(result) {
256                 var sectionParams = sections.map(function(section) {
257                     var sectionRoot = section[0];
258                     var sectionType = section[1];
259                     var totalCount = filterCountsByType(result.total, sectionType);
260                     var unreadCount = filterCountsByType(result.unread, sectionType);
262                     return [sectionRoot, totalCount, unreadCount];
263                 });
265                 // Open up one of the sections for the user.
266                 return openSection(sectionParams);
267             });
268     };
270     /**
271      * String describing this page used for aria-labels.
272      *
273      * @return {Object} jQuery promise
274      */
275     var description = function() {
276         return Str.get_string('messagedrawerviewoverview', 'core_message');
277     };
279     return {
280         show: show,
281         description: description
282     };
283 });