MDL-64715 message: improve the front-end for the self-conversations
[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     // 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     };
66     var loadAllCountsPromise = null;
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         }
83         return loadAllCountsPromise;
84     };
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;
99         if (types && types.length) {
100             total = types.reduce(function(carry, type) {
101                 return carry + counts.types[type];
102             }, total);
103         }
105         if (includeFavourites) {
106             total += counts.favourites;
107         }
109         return total;
110     };
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         });
128         if (isAlreadyOpen) {
129             // The user has already opened a section so there is nothing to do.
130             return;
131         }
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];
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         });
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     };
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     };
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     };
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;
195             if (count <= 0) {
196                 countContainer.addClass('hidden');
197             } else {
198                 countContainer.text(count);
199             }
200         };
201     };
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];
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         });
222         PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_ACCEPTED, decrementContactRequestCount(header));
223         PubSub.subscribe(MessageDrawerEvents.CONTACT_REQUEST_DECLINED, decrementContactRequestCount(header));
224     };
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         }
240         getSearchInput(header).val('');
241         var loggedInUserId = getLoggedInUserId(body);
242         var allCounts = loadAllCounts(loggedInUserId);
244         var sections = [
245             // Favourite conversations section.
246             [body.find(SELECTORS.FAVOURITES), OVERVIEW_SECTION_TYPES.FAVOURITE, true],
247             // Group conversations section.
248             [body.find(SELECTORS.GROUP_MESSAGES), OVERVIEW_SECTION_TYPES.PUBLIC, false],
249             // Private conversations section.
250             [body.find(SELECTORS.MESSAGES), OVERVIEW_SECTION_TYPES.PRIVATE, false]
251         ];
253         sections.forEach(function(args) {
254             var sectionRoot = args[0];
255             var sectionTypes = args[1];
256             var includeFavourites = args[2];
257             var totalCountPromise = allCounts.then(function(result) {
258                 return filterCountsByTypes(result.total, sectionTypes, includeFavourites);
259             });
260             var unreadCountPromise = allCounts.then(function(result) {
261                 return filterCountsByTypes(result.unread, sectionTypes, includeFavourites);
262             });
264             Section.show(namespace, null, sectionRoot, null, sectionTypes, includeFavourites,
265                 totalCountPromise, unreadCountPromise);
266         });
268         return allCounts.then(function(result) {
269                 var sectionParams = sections.map(function(section) {
270                     var sectionRoot = section[0];
271                     var sectionTypes = section[1];
272                     var includeFavourites = section[2];
273                     var totalCount = filterCountsByTypes(result.total, sectionTypes, includeFavourites);
274                     var unreadCount = filterCountsByTypes(result.unread, sectionTypes, includeFavourites);
276                     return [sectionRoot, totalCount, unreadCount];
277                 });
279                 // Open up one of the sections for the user.
280                 return openSection(sectionParams);
281             });
282     };
284     /**
285      * String describing this page used for aria-labels.
286      *
287      * @return {Object} jQuery promise
288      */
289     var description = function() {
290         return Str.get_string('messagedrawerviewoverview', 'core_message');
291     };
293     return {
294         show: show,
295         description: description
296     };
297 });