Merge branch '5.2' into 5.3
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLabSuite / Core / Notification / Handler.js
1 /**
2 * Provides desktop notifications via periodic polling with an
3 * increasing request delay on inactivity.
4 *
5 * @author Alexander Ebert
6 * @copyright 2001-2019 WoltLab GmbH
7 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
8 * @module WoltLabSuite/Core/Notification/Handler
9 */
10 define(['Ajax', 'Core', 'EventHandler', 'StringUtil'], function(Ajax, Core, EventHandler, StringUtil) {
11 "use strict";
12
13 if (!('Promise' in window) || !('Notification' in window)) {
14 // fake object exposed to ancient browsers (*cough* IE11 *cough*)
15 return {
16 setup: function () {}
17 }
18 }
19
20 var _allowNotification = false;
21 var _icon = '';
22 var _inactiveSince = 0;
23 //noinspection JSUnresolvedVariable
24 var _lastRequestTimestamp = window.TIME_NOW;
25 var _requestTimer = null;
26 var _sessionKeepAlive = 0;
27
28 /**
29 * @exports WoltLabSuite/Core/Notification/Handler
30 */
31 return {
32 /**
33 * Initializes the desktop notification system.
34 *
35 * @param {Object} options initialization options
36 */
37 setup: function (options) {
38 options = Core.extend({
39 enableNotifications: false,
40 icon: '',
41 sessionKeepAlive: 0
42 }, options);
43
44 _icon = options.icon;
45 _sessionKeepAlive = options.sessionKeepAlive * 60;
46
47 this._prepareNextRequest();
48
49 document.addEventListener('visibilitychange', this._onVisibilityChange.bind(this));
50 window.addEventListener('storage', this._onStorage.bind(this));
51
52 this._onVisibilityChange(null);
53
54 if (options.enableNotifications) {
55 switch (window.Notification.permission) {
56 case 'granted':
57 _allowNotification = true;
58 break;
59 case 'default':
60 window.Notification.requestPermission(function (result) {
61 if (result === 'granted') {
62 _allowNotification = true;
63 }
64 });
65 break;
66 }
67 }
68 },
69
70 /**
71 * Detects when this window is hidden or restored.
72 *
73 * @param {Event} event
74 * @protected
75 */
76 _onVisibilityChange: function(event) {
77 // document was hidden before
78 if (event !== null && !document.hidden) {
79 var difference = (Date.now() - _inactiveSince) / 60000;
80 if (difference > 4) {
81 this._resetTimer();
82 this._dispatchRequest();
83 }
84 }
85
86 _inactiveSince = (document.hidden) ? Date.now() : 0;
87 },
88
89 /**
90 * Returns the delay in minutes before the next request should be dispatched.
91 *
92 * @return {int}
93 * @protected
94 */
95 _getNextDelay: function() {
96 if (_inactiveSince === 0) return 5;
97
98 // milliseconds -> minutes
99 var inactiveMinutes = ~~((Date.now() - _inactiveSince) / 60000);
100 if (inactiveMinutes < 15) {
101 return 5;
102 }
103 else if (inactiveMinutes < 30) {
104 return 10;
105 }
106
107 return 15;
108 },
109
110 /**
111 * Resets the request delay timer.
112 *
113 * @protected
114 */
115 _resetTimer: function() {
116 if (_requestTimer !== null) {
117 window.clearTimeout(_requestTimer);
118 _requestTimer = null;
119 }
120 },
121
122 /**
123 * Schedules the next request using a calculated delay.
124 *
125 * @protected
126 */
127 _prepareNextRequest: function() {
128 this._resetTimer();
129
130 var delay = Math.min(this._getNextDelay(), _sessionKeepAlive);
131 _requestTimer = window.setTimeout(this._dispatchRequest.bind(this), delay * 60000);
132 },
133
134 /**
135 * Requests new data from the server.
136 *
137 * @protected
138 */
139 _dispatchRequest: function() {
140 var parameters = {};
141 EventHandler.fire('com.woltlab.wcf.notification', 'beforePoll', parameters);
142
143 // this timestamp is used to determine new notifications and to avoid
144 // notifications being displayed multiple times due to different origins
145 // (=subdomains) used, because we cannot synchronize them in the client
146 parameters.lastRequestTimestamp = _lastRequestTimestamp;
147
148 Ajax.api(this, {
149 parameters: parameters
150 });
151 },
152
153 /**
154 * Notifies subscribers for updated data received by another tab.
155 *
156 * @protected
157 */
158 _onStorage: function() {
159 // abort and re-schedule periodic request
160 this._prepareNextRequest();
161
162 var pollData, keepAliveData, abort = false;
163 try {
164 pollData = window.localStorage.getItem(Core.getStoragePrefix() + 'notification');
165 keepAliveData = window.localStorage.getItem(Core.getStoragePrefix() + 'keepAliveData');
166
167 pollData = JSON.parse(pollData);
168 keepAliveData = JSON.parse(keepAliveData);
169 }
170 catch (e) {
171 abort = true;
172 }
173
174 if (!abort) {
175 EventHandler.fire('com.woltlab.wcf.notification', 'onStorage', {
176 pollData: pollData,
177 keepAliveData: keepAliveData
178 });
179 }
180 },
181
182 _ajaxSuccess: function(data) {
183 var abort = false;
184 var keepAliveData = data.returnValues.keepAliveData;
185 var pollData = data.returnValues.pollData;
186
187 // forward keep alive data
188 window.WCF.System.PushNotification.executeCallbacks({returnValues: keepAliveData});
189
190 // store response data in local storage
191 try {
192 window.localStorage.setItem(Core.getStoragePrefix() + 'notification', JSON.stringify(pollData));
193 window.localStorage.setItem(Core.getStoragePrefix() + 'keepAliveData', JSON.stringify(keepAliveData));
194 }
195 catch (e) {
196 // storage is unavailable, e.g. in private mode, log error and disable polling
197 abort = true;
198
199 window.console.log(e);
200 }
201
202 if (!abort) {
203 this._prepareNextRequest();
204 }
205
206 _lastRequestTimestamp = data.returnValues.lastRequestTimestamp;
207
208 EventHandler.fire('com.woltlab.wcf.notification', 'afterPoll', pollData);
209
210 this._showNotification(pollData);
211 },
212
213 /**
214 * Displays a desktop notification.
215 *
216 * @param {Object} pollData
217 * @protected
218 */
219 _showNotification: function(pollData) {
220 if (!_allowNotification) {
221 return;
222 }
223
224 //noinspection JSUnresolvedVariable
225 if (typeof pollData.notification === 'object' && typeof pollData.notification.message === 'string') {
226 //noinspection JSUnresolvedVariable
227 var notification = new window.Notification(pollData.notification.title, {
228 body: StringUtil.unescapeHTML(pollData.notification.message).replace(/&#x202F;/g, "\u202F"),
229 icon: _icon
230 });
231 notification.onclick = function () {
232 window.focus();
233 notification.close();
234
235 //noinspection JSUnresolvedVariable
236 window.location = pollData.notification.link;
237 };
238 }
239 },
240
241 _ajaxSetup: function() {
242 //noinspection JSUnresolvedVariable
243 return {
244 data: {
245 actionName: 'poll',
246 className: 'wcf\\data\\session\\SessionAction'
247 },
248 ignoreError: !window.ENABLE_DEBUG_MODE,
249 silent: !window.ENABLE_DEBUG_MODE
250 };
251 }
252 }
253 });