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