MDL-62597 Privacy: Request date column should include time
[moodle.git] / message / output / popup / amd / src / notification_popover_controller.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 notification popover in the nav bar.
18  *
19  * See template: message_popup/notification_popover
20  *
21  * @module     message_popup/notification_popover_controller
22  * @class      notification_popover_controller
23  * @package    message_popup
24  * @copyright  2016 Ryan Wyllie <ryan@moodle.com>
25  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
27 define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',
28             'core/notification', 'core/custom_interaction_events', 'core/popover_region_controller',
29             'message_popup/notification_repository', 'message_popup/notification_area_events'],
30         function($, Ajax, Templates, Str, URL, DebugNotification, CustomEvents,
31             PopoverController, NotificationRepo, NotificationAreaEvents) {
33     var SELECTORS = {
34         MARK_ALL_READ_BUTTON: '[data-action="mark-all-read"]',
35         ALL_NOTIFICATIONS_CONTAINER: '[data-region="all-notifications"]',
36         NOTIFICATION: '[data-region="notification-content-item-container"]',
37         UNREAD_NOTIFICATION: '[data-region="notification-content-item-container"].unread',
38         NOTIFICATION_LINK: '[data-action="content-item-link"]',
39         EMPTY_MESSAGE: '[data-region="empty-message"]',
40         COUNT_CONTAINER: '[data-region="count-container"]',
41     };
43     /**
44      * Constructor for the NotificationPopoverController.
45      * Extends PopoverRegionController.
46      *
47      * @param {object} element jQuery object root element of the popover
48      */
49     var NotificationPopoverController = function(element) {
50         // Initialise base class.
51         PopoverController.call(this, element);
53         this.markAllReadButton = this.root.find(SELECTORS.MARK_ALL_READ_BUTTON);
54         this.unreadCount = 0;
55         this.userId = this.root.attr('data-userid');
56         this.container = this.root.find(SELECTORS.ALL_NOTIFICATIONS_CONTAINER);
57         this.limit = 20;
58         this.offset = 0;
59         this.loadedAll = false;
60         this.initialLoad = false;
62         // Let's find out how many unread notifications there are.
63         this.loadUnreadNotificationCount();
64     };
66     /**
67      * Clone the parent prototype.
68      */
69     NotificationPopoverController.prototype = Object.create(PopoverController.prototype);
71     /**
72      * Make sure the constructor is set correctly.
73      */
74     NotificationPopoverController.prototype.constructor = NotificationPopoverController;
76     /**
77      * Set the correct aria label on the menu toggle button to be read out by screen
78      * readers. The message will indicate the state of the unread notifications.
79      *
80      * @method updateButtonAriaLabel
81      */
82     NotificationPopoverController.prototype.updateButtonAriaLabel = function() {
83         if (this.isMenuOpen()) {
84             Str.get_string('hidenotificationwindow', 'message').done(function(string) {
85                 this.menuToggle.attr('aria-label', string);
86             }.bind(this));
87         } else {
88             if (this.unreadCount) {
89                 Str.get_string('shownotificationwindowwithcount', 'message', this.unreadCount).done(function(string) {
90                     this.menuToggle.attr('aria-label', string);
91                 }.bind(this));
92             } else {
93                 Str.get_string('shownotificationwindownonew', 'message').done(function(string) {
94                     this.menuToggle.attr('aria-label', string);
95                 }.bind(this));
96             }
97         }
98     };
100     /**
101      * Return the jQuery element with the content. This will return either
102      * the unread notification container or the all notification container
103      * depending on which is currently visible.
104      *
105      * @method getContent
106      * @return {object} jQuery object currently visible content contianer
107      */
108     NotificationPopoverController.prototype.getContent = function() {
109         return this.container;
110     };
112     /**
113      * Get the offset value for the current state of the popover in order
114      * to sent to the backend to correctly paginate the notifications.
115      *
116      * @method getOffset
117      * @return {int} current offset
118      */
119     NotificationPopoverController.prototype.getOffset = function() {
120         return this.offset;
121     };
123     /**
124      * Increment the offset for the current state, if required.
125      *
126      * @method incrementOffset
127      */
128     NotificationPopoverController.prototype.incrementOffset = function() {
129         this.offset += this.limit;
130     };
132     /**
133      * Check if the first load of notification has been triggered for the current
134      * state of the popover.
135      *
136      * @method hasDoneInitialLoad
137      * @return {bool} true if first notification loaded, false otherwise
138      */
139     NotificationPopoverController.prototype.hasDoneInitialLoad = function() {
140         return this.initialLoad;
141     };
143     /**
144      * Check if we've loaded all of the notifications for the current popover
145      * state.
146      *
147      * @method hasLoadedAllContent
148      * @return {bool} true if all notifications loaded, false otherwise
149      */
150     NotificationPopoverController.prototype.hasLoadedAllContent = function() {
151         return this.loadedAll;
152     };
154     /**
155      * Set the state of the loaded all content property for the current state
156      * of the popover.
157      *
158      * @method setLoadedAllContent
159      * @param {bool} val True if all content is loaded, false otherwise
160      */
161     NotificationPopoverController.prototype.setLoadedAllContent = function(val) {
162         this.loadedAll = val;
163     };
165     /**
166      * Show the unread notification count badge on the menu toggle if there
167      * are unread notifications, otherwise hide it.
168      *
169      * @method renderUnreadCount
170      */
171     NotificationPopoverController.prototype.renderUnreadCount = function() {
172         var element = this.root.find(SELECTORS.COUNT_CONTAINER);
174         if (this.unreadCount) {
175             element.text(this.unreadCount);
176             element.removeClass('hidden');
177         } else {
178             element.addClass('hidden');
179         }
180     };
182     /**
183      * Hide the unread notification count badge on the menu toggle.
184      *
185      * @method hideUnreadCount
186      */
187     NotificationPopoverController.prototype.hideUnreadCount = function() {
188         this.root.find(SELECTORS.COUNT_CONTAINER).addClass('hidden');
189     };
191     /**
192      * Ask the server how many unread notifications are left, render the value
193      * as a badge on the menu toggle and update the aria labels on the menu
194      * toggle.
195      *
196      * @method loadUnreadNotificationCount
197      */
198     NotificationPopoverController.prototype.loadUnreadNotificationCount = function() {
199         NotificationRepo.countUnread({useridto: this.userId}).then(function(count) {
200             this.unreadCount = count;
201             this.renderUnreadCount();
202             this.updateButtonAriaLabel();
203         }.bind(this)).catch(DebugNotification.exception);
204     };
206     /**
207      * Find the notification element for the given id.
208      *
209      * @param {int} id
210      * @method getNotificationElement
211      * @return {object|null} The notification element
212      */
213     NotificationPopoverController.prototype.getNotificationElement = function(id) {
214         var element = this.root.find(SELECTORS.NOTIFICATION + '[data-id="' + id + '"]');
215         return element.length == 1 ? element : null;
216     };
218     /**
219      * Render the notification data with the appropriate template and add it to the DOM.
220      *
221      * @method renderNotifications
222      * @param {array} notifications Notification data
223      * @param {object} container jQuery object the container to append the rendered notifications
224      * @return {object} jQuery promise that is resolved when all notifications have been
225      *                  rendered and added to the DOM
226      */
227     NotificationPopoverController.prototype.renderNotifications = function(notifications, container) {
228         var promises = [];
230         $.each(notifications, function(index, notification) {
231             // Determine what the offset was when loading this notification.
232             var offset = this.getOffset() - this.limit;
233             // Update the view more url to contain the offset to allow the notifications
234             // page to load to the correct position in the list of notifications.
235             notification.viewmoreurl = URL.relativeUrl('/message/output/popup/notifications.php', {
236                 notificationid: notification.id,
237                 offset: offset,
238             });
240             // Link to mark read page before loading the actual link.
241             notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', {
242                 redirecturl: notification.contexturl,
243                 notificationid: notification.id,
244             });
246             var promise = Templates.render('message_popup/notification_content_item', notification)
247             .then(function(html, js) {
248                 return {html: html, js: js};
249             });
250             promises.push(promise);
251         }.bind(this));
253         return $.when.apply($, promises).then(function() {
254             // Each of the promises in the when will pass its results as an argument to the function.
255             // The order of the arguments will be the order that the promises are passed to when()
256             // i.e. the first promise's results will be in the first argument.
257             $.each(arguments, function(index, argument) {
258                 container.append(argument.html);
259                 Templates.runTemplateJS(argument.js);
260             });
261             return;
262         });
263     };
265     /**
266      * Send a request for more notifications from the server, if we aren't already
267      * loading some and haven't already loaded all of them.
268      *
269      * Takes into account the current mode of the popover and will request only
270      * unread notifications if required.
271      *
272      * All notifications are marked as read by the server when they are returned.
273      *
274      * @method loadMoreNotifications
275      * @return {object} jQuery promise that is resolved when notifications have been
276      *                        retrieved and added to the DOM
277      */
278     NotificationPopoverController.prototype.loadMoreNotifications = function() {
279         if (this.isLoading || this.hasLoadedAllContent()) {
280             return $.Deferred().resolve();
281         }
283         this.startLoading();
284         var request = {
285             limit: this.limit,
286             offset: this.getOffset(),
287             useridto: this.userId,
288         };
290         var container = this.getContent();
291         return NotificationRepo.query(request).then(function(result) {
292             var notifications = result.notifications;
293             this.unreadCount = result.unreadcount;
294             this.setLoadedAllContent(!notifications.length || notifications.length < this.limit);
295             this.initialLoad = true;
296             this.updateButtonAriaLabel();
298             if (notifications.length) {
299                 this.incrementOffset();
300                 return this.renderNotifications(notifications, container);
301             }
303             return false;
304         }.bind(this))
305         .always(function() {
306             this.stopLoading();
307         }.bind(this));
308     };
310     /**
311      * Send a request to the server to mark all unread notifications as read and update
312      * the unread count and unread notification elements appropriately.
313      *
314      * @return {Promise}
315      * @method markAllAsRead
316      */
317     NotificationPopoverController.prototype.markAllAsRead = function() {
318         this.markAllReadButton.addClass('loading');
320         return NotificationRepo.markAllAsRead({useridto: this.userId})
321             .then(function() {
322                 this.unreadCount = 0;
323                 this.root.find(SELECTORS.UNREAD_NOTIFICATION).removeClass('unread');
324             }.bind(this))
325             .always(function() {
326                 this.markAllReadButton.removeClass('loading');
327             }.bind(this));
328     };
330     /**
331      * Add all of the required event listeners for this notification popover.
332      *
333      * @method registerEventListeners
334      */
335     NotificationPopoverController.prototype.registerEventListeners = function() {
336         CustomEvents.define(this.root, [
337             CustomEvents.events.activate,
338         ]);
340         // Mark all notifications read if the user activates the mark all as read button.
341         this.root.on(CustomEvents.events.activate, SELECTORS.MARK_ALL_READ_BUTTON, function(e, data) {
342             this.markAllAsRead();
343             e.stopPropagation();
344             data.originalEvent.preventDefault();
345         }.bind(this));
347         // Mark individual notification read if the user activates it.
348         this.root.on(CustomEvents.events.activate, SELECTORS.NOTIFICATION_LINK, function(e) {
349             var element = $(e.target).closest(SELECTORS.NOTIFICATION);
351             if (element.hasClass('unread')) {
352                 this.unreadCount--;
353                 element.removeClass('unread');
354             }
356             e.stopPropagation();
357         }.bind(this));
359         // Update the notification information when the menu is opened.
360         this.root.on(this.events().menuOpened, function() {
361             this.hideUnreadCount();
362             this.updateButtonAriaLabel();
364             if (!this.hasDoneInitialLoad()) {
365                 this.loadMoreNotifications();
366             }
367         }.bind(this));
369         // Update the unread notification count when the menu is closed.
370         this.root.on(this.events().menuClosed, function() {
371             this.renderUnreadCount();
372             this.updateButtonAriaLabel();
373         }.bind(this));
375         // Set aria attributes when popover is loading.
376         this.root.on(this.events().startLoading, function() {
377             this.getContent().attr('aria-busy', 'true');
378         }.bind(this));
380         // Set aria attributes when popover is finished loading.
381         this.root.on(this.events().stopLoading, function() {
382             this.getContent().attr('aria-busy', 'false');
383         }.bind(this));
385         // Load more notifications if the user has scrolled to the end of content
386         // item list.
387         this.getContentContainer().on(CustomEvents.events.scrollBottom, function() {
388             if (!this.isLoading && !this.hasLoadedAllContent()) {
389                 this.loadMoreNotifications();
390             }
391         }.bind(this));
393         // Stop mouse scroll from propagating to the window element and
394         // scrolling the page.
395         CustomEvents.define(this.getContentContainer(), [
396             CustomEvents.events.scrollLock
397         ]);
399         // Listen for when a notification is shown in the notifications page and mark
400         // it as read, if it's unread.
401         $(document).on(NotificationAreaEvents.notificationShown, function(e, notification) {
402             if (!notification.read) {
403                 var element = this.getNotificationElement(notification.id);
405                 if (element) {
406                     element.removeClass('unread');
407                 }
409                 this.unreadCount--;
410                 this.renderUnreadCount();
411             }
412         }.bind(this));
413     };
415     return NotificationPopoverController;
416 });