Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Delta Between Two Patch Sets: lib/notification.js

Issue 5330039625220096: Issue 1162 - Cache notification URL matcher
Left Patch Set: Created Oct. 23, 2014, 2:09 p.m.
Right Patch Set: Fixed regression: Error if notification data not yet initialized Created Jan. 29, 2018, 3:36 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « no previous file | no next file » | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 /* 1 /*
2 * This file is part of Adblock Plus <http://adblockplus.org/>, 2 * This file is part of Adblock Plus <https://adblockplus.org/>,
3 * Copyright (C) 2006-2014 Eyeo GmbH 3 * Copyright (C) 2006-present eyeo GmbH
4 * 4 *
5 * Adblock Plus is free software: you can redistribute it and/or modify 5 * Adblock Plus is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 3 as 6 * it under the terms of the GNU General Public License version 3 as
7 * published by the Free Software Foundation. 7 * published by the Free Software Foundation.
8 * 8 *
9 * Adblock Plus is distributed in the hope that it will be useful, 9 * Adblock Plus is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details. 12 * GNU General Public License for more details.
13 * 13 *
14 * You should have received a copy of the GNU General Public License 14 * You should have received a copy of the GNU General Public License
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
16 */ 16 */
17 17
18 "use strict";
19
18 /** 20 /**
19 * @fileOverview Handles notifications. 21 * @fileOverview Handles notifications.
20 */ 22 */
21 23
22 Cu.import("resource://gre/modules/Services.jsm"); 24 const {Prefs} = require("prefs");
23 25 const {Downloader, Downloadable,
24 let {Prefs} = require("prefs"); 26 MILLIS_IN_MINUTE, MILLIS_IN_HOUR,
25 let {Downloader, Downloadable, MILLIS_IN_MINUTE, MILLIS_IN_HOUR, MILLIS_IN_DAY} = require("downloader"); 27 MILLIS_IN_DAY} = require("./downloader");
26 let {Utils} = require("utils"); 28 const {Utils} = require("utils");
27 let {Matcher} = require("matcher"); 29 const {Matcher, defaultMatcher} = require("./matcher");
28 let {Filter} = require("filterClasses"); 30 const {Filter, RegExpFilter, WhitelistFilter} = require("./filterClasses");
29 31
30 let INITIAL_DELAY = 12 * MILLIS_IN_MINUTE; 32 const INITIAL_DELAY = 1 * MILLIS_IN_MINUTE;
31 let CHECK_INTERVAL = 1 * MILLIS_IN_HOUR; 33 const CHECK_INTERVAL = 1 * MILLIS_IN_HOUR;
32 let EXPIRATION_INTERVAL = 1 * MILLIS_IN_DAY; 34 const EXPIRATION_INTERVAL = 1 * MILLIS_IN_DAY;
33 let TYPE = { 35 const TYPE = {
34 information: 0, 36 information: 0,
35 question: 1, 37 question: 1,
36 critical: 2 38 relentless: 2,
39 critical: 3
37 }; 40 };
38 41 const MATCHER = Symbol("Notification matcher");
39 let listeners = {}; 42
43 let showListeners = [];
44 let questionListeners = {};
40 45
41 function getNumericalSeverity(notification) 46 function getNumericalSeverity(notification)
42 { 47 {
43 return (notification.type in TYPE ? TYPE[notification.type] : TYPE.information ); 48 if (notification.type in TYPE)
49 return TYPE[notification.type];
50 return TYPE.information;
44 } 51 }
45 52
46 function saveNotificationData() 53 function saveNotificationData()
47 { 54 {
48 // HACK: JSON values aren't saved unless they are assigned a different object. 55 // HACK: JSON values aren't saved unless they are assigned a different object.
49 Prefs.notificationdata = JSON.parse(JSON.stringify(Prefs.notificationdata)); 56 Prefs.notificationdata = JSON.parse(JSON.stringify(Prefs.notificationdata));
50 } 57 }
51 58
52 function localize(translations, locale) 59 function localize(translations, locale)
53 { 60 {
54 if (locale in translations) 61 if (locale in translations)
55 return translations[locale]; 62 return translations[locale];
56 63
57 let languagePart = locale.substring(0, locale.indexOf("-")); 64 let languagePart = locale.substring(0, locale.indexOf("-"));
58 if (languagePart && languagePart in translations) 65 if (languagePart && languagePart in translations)
59 return translations[languagePart]; 66 return translations[languagePart];
60 67
61 let defaultLocale = "en-US"; 68 let defaultLocale = "en-US";
62 return translations[defaultLocale]; 69 return translations[defaultLocale];
63 } 70 }
64 71
72 function parseVersionComponent(comp)
73 {
74 if (comp == "*")
75 return Infinity;
76 return parseInt(comp, 10) || 0;
77 }
78
79 function compareVersion(v1, v2)
80 {
81 let regexp = /^(.*?)([a-z].*)?$/i;
82 let [, head1, tail1] = regexp.exec(v1);
83 let [, head2, tail2] = regexp.exec(v2);
84 let components1 = head1.split(".");
85 let components2 = head2.split(".");
86
87 for (let i = 0; i < components1.length ||
88 i < components2.length; i++)
89 {
90 let result = parseVersionComponent(components1[i]) -
91 parseVersionComponent(components2[i]) || 0;
92
93 if (result != 0)
94 return result;
95 }
96
97 // Compare version suffix (e.g. 0.1alpha < 0.1b1 < 01.b2 < 0.1).
98 // However, note that this is a simple string comparision, meaning: b10 < b2
99 if (tail1 == tail2)
100 return 0;
101 if (!tail1 || tail2 && tail1 > tail2)
102 return 1;
103 return -1;
104 }
105
106 /**
107 * Initializes notification's matcher based on notification's URL filters
108 * @param {Object} notification
109 */
65 function initNotificationMatcher(notification) 110 function initNotificationMatcher(notification)
66 { 111 {
67 if ("_matcher" in notification || !(notification.urlFilters instanceof Array)) 112 if (MATCHER in notification || !(notification.urlFilters instanceof Array))
68 return; 113 return;
69 114
70 let matcher = new Matcher(); 115 let matcher = new Matcher();
71 for (let urlFilter of notification.urlFilters) 116 for (let urlFilter of notification.urlFilters)
117 {
72 matcher.add(Filter.fromText(urlFilter)); 118 matcher.add(Filter.fromText(urlFilter));
73 matcher.toJSON = () => {}; 119 }
sergei 2018/01/03 11:12:22 This is not exactly how it's said in the issue "No
Thomas Greiner 2018/01/11 18:18:19 Fortunately, we can now use `Symbol` instead so th
kzar 2018/01/12 12:27:17 Acknowledged.
74 notification._matcher = matcher; 120 notification[MATCHER] = matcher;
121 }
122
123 /**
124 * Matches URL against notification's URL filters
125 * @param {Object} notification
126 * @param {string} [url]
127 * @return {boolean} whether notification and URL match
128 */
129 function matchesUrl(notification, url)
130 {
131 // No matching necessary if there's nothing to match
132 if (typeof url !== "string" && !(MATCHER in notification))
133 return true;
134
135 // Notification shouldn't match if extension is disabled
136 if (!Prefs.enabled)
137 return false;
138
139 // Notification shouldn't match if matching cannot be done
140 if (typeof url !== "string" || !(MATCHER in notification))
141 return false;
sergei 2018/01/30 17:25:05 It somehow conflicts with above if (typeof url !==
Thomas Greiner 2018/01/30 19:17:53 The logic behind it is that if the notification do
sergei 2018/01/30 21:06:48 Acknowledged.
142
143 let host;
144 try
145 {
146 host = new URL(url).hostname;
147 }
148 catch (e)
149 {
150 host = "";
151 }
152
153 // Notification shouldn't match if extension is disabled on provided domain
154 let exception = defaultMatcher.matchesAny(
155 url, RegExpFilter.typeMap.DOCUMENT, host, false, null
156 );
157 if (exception instanceof WhitelistFilter)
158 return false;
159
160 // Notification should match if one of its filters matches
161 let filter = notification[MATCHER].matchesAny(
162 url, RegExpFilter.typeMap.DOCUMENT, host, false, null
163 );
164 return !!filter;
75 } 165 }
76 166
77 /** 167 /**
78 * The object providing actual downloading functionality. 168 * The object providing actual downloading functionality.
79 * @type Downloader 169 * @type {Downloader}
80 */ 170 */
81 let downloader = null; 171 let downloader = null;
172
173 /**
174 * List of notifications provided by the extension
175 * @type {Object[]}
176 */
82 let localData = []; 177 let localData = [];
83 let remoteData = [];
sergei 2018/01/03 11:12:22 Could you please add a comment that localData and
Thomas Greiner 2018/01/11 18:18:19 Yep, will do.
84 178
85 /** 179 /**
86 * Regularly fetches notifications and decides which to show. 180 * Regularly fetches notifications and decides which to show.
87 * @class 181 * @class
88 */ 182 */
89 let Notification = exports.Notification = 183 let Notification = exports.Notification =
90 { 184 {
91 /** 185 /**
92 * Called on module startup. 186 * Called on module startup.
93 */ 187 */
94 init: function() 188 init()
95 { 189 {
96 let notificationdata = Prefs.notificationdata.data; 190 let {data} = Prefs.notificationdata;
sergei 2018/01/30 17:25:05 Prefs' properties can be not ready yet, I would re
sergei 2018/01/30 17:28:24 Perhaps alternatively we could do it in `matchesUr
Thomas Greiner 2018/01/30 19:17:53 In theory that's correct but in practice this is n
sergei 2018/01/30 21:06:48 It does happen on practice (at least I observed th
kzar 2018/01/31 11:06:27 How about using the Prefs.untilLoaded Promise?
sergei 2018/02/05 12:49:34 It seems the best option.
97 if (notificationdata) 191 if (data)
98 { 192 {
99 for (let notification of notificationdata.notifications) 193 for (let notification of data.notifications)
194 {
100 initNotificationMatcher(notification); 195 initNotificationMatcher(notification);
101 remoteData = notificationdata.notifications; 196 }
102 } 197 }
103 198
104 downloader = new Downloader(this._getDownloadables.bind(this), INITIAL_DELAY , CHECK_INTERVAL); 199 downloader = new Downloader(this._getDownloadables.bind(this),
105 onShutdown.add(function() 200 INITIAL_DELAY, CHECK_INTERVAL);
106 {
107 downloader.cancel();
108 });
109
110 downloader.onExpirationChange = this._onExpirationChange.bind(this); 201 downloader.onExpirationChange = this._onExpirationChange.bind(this);
111 downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this); 202 downloader.onDownloadSuccess = this._onDownloadSuccess.bind(this);
112 downloader.onDownloadError = this._onDownloadError.bind(this); 203 downloader.onDownloadError = this._onDownloadError.bind(this);
204 onShutdown.add(() => downloader.cancel());
113 }, 205 },
114 206
115 /** 207 /**
116 * Yields a Downloadable instances for the notifications download. 208 * Yields a Downloadable instances for the notifications download.
117 */ 209 */
118 _getDownloadables: function() 210 *_getDownloadables()
kzar 2018/01/04 17:04:09 Why are we exporting functions like this and prefi
Thomas Greiner 2018/01/11 18:18:20 I don't think it's meant to be exported because it
119 { 211 {
120 let downloadable = new Downloadable(Prefs.notificationurl); 212 let downloadable = new Downloadable(Prefs.notificationurl);
121 if (typeof Prefs.notificationdata.lastError === "number") 213 if (typeof Prefs.notificationdata.lastError === "number")
122 downloadable.lastError = Prefs.notificationdata.lastError; 214 downloadable.lastError = Prefs.notificationdata.lastError;
123 if (typeof Prefs.notificationdata.lastCheck === "number") 215 if (typeof Prefs.notificationdata.lastCheck === "number")
124 downloadable.lastCheck = Prefs.notificationdata.lastCheck; 216 downloadable.lastCheck = Prefs.notificationdata.lastCheck;
125 if (typeof Prefs.notificationdata.data === "object" && "version" in Prefs.no tificationdata.data) 217 if (typeof Prefs.notificationdata.data === "object" &&
218 "version" in Prefs.notificationdata.data)
219 {
126 downloadable.lastVersion = Prefs.notificationdata.data.version; 220 downloadable.lastVersion = Prefs.notificationdata.data.version;
221 }
127 if (typeof Prefs.notificationdata.softExpiration === "number") 222 if (typeof Prefs.notificationdata.softExpiration === "number")
128 downloadable.softExpiration = Prefs.notificationdata.softExpiration; 223 downloadable.softExpiration = Prefs.notificationdata.softExpiration;
129 if (typeof Prefs.notificationdata.hardExpiration === "number") 224 if (typeof Prefs.notificationdata.hardExpiration === "number")
130 downloadable.hardExpiration = Prefs.notificationdata.hardExpiration; 225 downloadable.hardExpiration = Prefs.notificationdata.hardExpiration;
226 if (typeof Prefs.notificationdata.downloadCount === "number")
227 downloadable.downloadCount = Prefs.notificationdata.downloadCount;
131 yield downloadable; 228 yield downloadable;
132 }, 229 },
133 230
134 _onExpirationChange: function(downloadable) 231 _onExpirationChange(downloadable)
135 { 232 {
136 Prefs.notificationdata.lastCheck = downloadable.lastCheck; 233 Prefs.notificationdata.lastCheck = downloadable.lastCheck;
137 Prefs.notificationdata.softExpiration = downloadable.softExpiration; 234 Prefs.notificationdata.softExpiration = downloadable.softExpiration;
138 Prefs.notificationdata.hardExpiration = downloadable.hardExpiration; 235 Prefs.notificationdata.hardExpiration = downloadable.hardExpiration;
139 saveNotificationData(); 236 saveNotificationData();
140 }, 237 },
141 238
142 _onDownloadSuccess: function(downloadable, responseText, errorCallback, redire ctCallback) 239 _onDownloadSuccess(downloadable, responseText, errorCallback,
240 redirectCallback)
143 { 241 {
144 try 242 try
145 { 243 {
146 let data = JSON.parse(responseText); 244 let data = JSON.parse(responseText);
147 for (let notification of data.notifications) 245 for (let notification of data.notifications)
148 { 246 {
149 if ("severity" in notification) 247 if ("severity" in notification)
150 { 248 {
151 if (!("type" in notification)) 249 if (!("type" in notification))
152 notification.type = notification.severity; 250 notification.type = notification.severity;
153 delete notification.severity; 251 delete notification.severity;
154 } 252 }
155 initNotificationMatcher(notification); 253 initNotificationMatcher(notification);
156 } 254 }
157 remoteData = data.notifications;
sergei 2018/01/03 11:12:22 It's a bit theoretical but IMO it can be a technic
kzar 2018/01/04 17:04:09 Yea, I don't think these changes are necessary now
Thomas Greiner 2018/01/11 18:18:19 Good point. I don't remember why I made this chang
158 Prefs.notificationdata.data = data; 255 Prefs.notificationdata.data = data;
sergei 2018/01/03 11:12:22 Despite it's not directly a part of this issue, sh
kzar 2018/01/04 17:04:09 Hmm, I'm not too familiar with this code but I thi
Thomas Greiner 2018/01/11 18:18:19 By overriding existing notifications we can contro
159 } 256 }
160 catch (e) 257 catch (e)
161 { 258 {
162 Cu.reportError(e); 259 Cu.reportError(e);
163 errorCallback("synchronize_invalid_data"); 260 errorCallback("synchronize_invalid_data");
164 return; 261 return;
165 } 262 }
166 263
167 Prefs.notificationdata.lastError = 0; 264 Prefs.notificationdata.lastError = 0;
168 Prefs.notificationdata.downloadStatus = "synchronize_ok"; 265 Prefs.notificationdata.downloadStatus = "synchronize_ok";
169 [Prefs.notificationdata.softExpiration, Prefs.notificationdata.hardExpiratio n] = downloader.processExpirationInterval(EXPIRATION_INTERVAL); 266 [
267 Prefs.notificationdata.softExpiration,
268 Prefs.notificationdata.hardExpiration
269 ] = downloader.processExpirationInterval(EXPIRATION_INTERVAL);
270 Prefs.notificationdata.downloadCount = downloadable.downloadCount;
170 saveNotificationData(); 271 saveNotificationData();
171 }, 272
172 273 Notification.showNext();
173 _onDownloadError: function(downloadable, downloadURL, error, channelStatus, re sponseStatus, redirectCallback) 274 },
275
276 _onDownloadError(downloadable, downloadURL, error, channelStatus,
277 responseStatus, redirectCallback)
174 { 278 {
175 Prefs.notificationdata.lastError = Date.now(); 279 Prefs.notificationdata.lastError = Date.now();
176 Prefs.notificationdata.downloadStatus = error; 280 Prefs.notificationdata.downloadStatus = error;
177 saveNotificationData(); 281 saveNotificationData();
178 }, 282 },
179 283
180 /** 284 /**
285 * Adds a listener for notifications to be shown.
286 * @param {Function} listener Listener to be invoked when a notification is
287 * to be shown
288 */
289 addShowListener(listener)
290 {
291 if (showListeners.indexOf(listener) == -1)
292 showListeners.push(listener);
293 },
294
295 /**
296 * Removes the supplied listener.
297 * @param {Function} listener Listener that was added via addShowListener()
298 */
299 removeShowListener(listener)
300 {
301 let index = showListeners.indexOf(listener);
302 if (index != -1)
303 showListeners.splice(index, 1);
304 },
305
306 /**
181 * Determines which notification is to be shown next. 307 * Determines which notification is to be shown next.
182 * @param {String} url URL to match notifications to (optional) 308 * @param {string} url URL to match notifications to (optional)
183 * @return {Object} notification to be shown, or null if there is none 309 * @return {Object} notification to be shown, or null if there is none
184 */ 310 */
185 getNextToShow: function(url) 311 _getNextToShow(url)
186 { 312 {
187 function checkTarget(target, parameter, name, version) 313 let remoteData = [];
188 { 314 if (typeof Prefs.notificationdata.data == "object" &&
189 let minVersionKey = parameter + "MinVersion"; 315 Prefs.notificationdata.data.notifications instanceof Array)
190 let maxVersionKey = parameter + "MaxVersion"; 316 {
191 return !((parameter in target && target[parameter] != name) || 317 remoteData = Prefs.notificationdata.data.notifications;
192 (minVersionKey in target && Services.vc.compare(version, target[m inVersionKey]) < 0) || 318 }
193 (maxVersionKey in target && Services.vc.compare(version, target[m axVersionKey]) > 0));
194 }
195
196 if (!(Prefs.notificationdata.shown instanceof Array))
197 {
198 Prefs.notificationdata.shown = [];
199 saveNotificationData();
200 }
201
202 let notifications = localData.concat(remoteData); 319 let notifications = localData.concat(remoteData);
203 if (notifications.length === 0) 320 if (notifications.length === 0)
204 return null; 321 return null;
205 322
206 let {addonName, addonVersion, application, applicationVersion, platform, pla tformVersion} = require("info"); 323 const {addonName, addonVersion, application,
324 applicationVersion, platform, platformVersion} = require("info");
325
326 let targetChecks = {
327 extension: v => v == addonName,
328 extensionMinVersion:
329 v => compareVersion(addonVersion, v) >= 0,
330 extensionMaxVersion:
331 v => compareVersion(addonVersion, v) <= 0,
332 application: v => v == application,
333 applicationMinVersion:
334 v => compareVersion(applicationVersion, v) >= 0,
335 applicationMaxVersion:
336 v => compareVersion(applicationVersion, v) <= 0,
337 platform: v => v == platform,
338 platformMinVersion:
339 v => compareVersion(platformVersion, v) >= 0,
340 platformMaxVersion:
341 v => compareVersion(platformVersion, v) <= 0,
342 blockedTotalMin: v => Prefs.show_statsinpopup &&
343 Prefs.blocked_total >= v,
344 blockedTotalMax: v => Prefs.show_statsinpopup &&
345 Prefs.blocked_total <= v,
346 locales: v => v.includes(Utils.appLocale)
347 };
348
207 let notificationToShow = null; 349 let notificationToShow = null;
208 for (let notification of notifications) 350 for (let notification of notifications)
209 { 351 {
210 if ((typeof notification.type === "undefined" || notification.type !== "cr itical") 352 if (typeof notification.type === "undefined" ||
211 && Prefs.notificationdata.shown.indexOf(notification.id) !== -1) 353 notification.type !== "critical")
212 continue;
213
214 if (typeof url === "string" || "_matcher" in notification)
215 { 354 {
216 if (typeof url === "string" && "_matcher" in notification) 355 let shown;
356 if (typeof Prefs.notificationdata.shown == "object")
357 shown = Prefs.notificationdata.shown[notification.id];
358
359 if (typeof shown != "undefined")
217 { 360 {
218 if (!notification._matcher.matchesAny(url, "DOCUMENT", url)) 361 if (typeof notification.interval == "number")
362 {
363 if (shown + notification.interval > Date.now())
364 continue;
365 }
366 else if (shown)
219 continue; 367 continue;
220 } 368 }
221 else 369
370 if (notification.type !== "relentless" &&
371 Prefs.notifications_ignoredcategories.indexOf("*") != -1)
372 {
222 continue; 373 continue;
374 }
223 } 375 }
376
377 if (!matchesUrl(notification, url))
378 continue;
224 379
225 if (notification.targets instanceof Array) 380 if (notification.targets instanceof Array)
226 { 381 {
227 let match = false; 382 let match = false;
383
228 for (let target of notification.targets) 384 for (let target of notification.targets)
229 { 385 {
230 if (checkTarget(target, "extension", addonName, addonVersion) && 386 if (Object.keys(target).every(key =>
231 checkTarget(target, "application", application, applicationVersion ) && 387 targetChecks.hasOwnProperty(key) &&
232 checkTarget(target, "platform", platform, platformVersion)) 388 targetChecks[key](target[key])))
233 { 389 {
234 match = true; 390 match = true;
235 break; 391 break;
236 } 392 }
237 } 393 }
238 if (!match) 394 if (!match)
395 {
239 continue; 396 continue;
397 }
240 } 398 }
241 399
242 if (!notificationToShow 400 if (!notificationToShow ||
243 || getNumericalSeverity(notification) > getNumericalSeverity(notificat ionToShow)) 401 getNumericalSeverity(notification) >
402 getNumericalSeverity(notificationToShow))
244 notificationToShow = notification; 403 notificationToShow = notification;
245 } 404 }
246 405
247 if (notificationToShow && "id" in notificationToShow)
248 {
249 if (notificationToShow.type !== "question")
250 this.markAsShown(notificationToShow.id);
251 }
252
253 return notificationToShow; 406 return notificationToShow;
254 }, 407 },
255 408
256 markAsShown: function(id) 409 /**
257 { 410 * Invokes the listeners added via addShowListener() with the next
258 if (Prefs.notificationdata.shown.indexOf(id) > -1) 411 * notification to be shown.
259 return; 412 * @param {string} url URL to match notifications to (optional)
260 413 */
261 Prefs.notificationdata.shown.push(id); 414 showNext(url)
415 {
416 let notification = Notification._getNextToShow(url);
417 if (notification)
418 {
419 for (let showListener of showListeners)
420 showListener(notification);
421 }
422 },
423
424 /**
425 * Marks a notification as shown.
426 * @param {string} id ID of the notification to be marked as shown
427 */
428 markAsShown(id)
429 {
430 let now = Date.now();
431 let data = Prefs.notificationdata;
432
433 if (data.shown instanceof Array)
434 {
435 let newShown = {};
436 for (let oldId of data.shown)
437 newShown[oldId] = now;
438 data.shown = newShown;
439 }
440
441 if (typeof data.shown != "object")
442 data.shown = {};
443
444 data.shown[id] = now;
445
262 saveNotificationData(); 446 saveNotificationData();
263 }, 447 },
264 448
265 /** 449 /**
266 * Localizes the texts of the supplied notification. 450 * Localizes the texts of the supplied notification.
267 * @param {Object} notification notification to translate 451 * @param {Object} notification notification to translate
268 * @param {String} locale the target locale (optional, defaults to the 452 * @param {string} locale the target locale (optional, defaults to the
269 * application locale) 453 * application locale)
270 * @return {Object} the translated texts 454 * @return {Object} the translated texts
271 */ 455 */
272 getLocalizedTexts: function(notification, locale) 456 getLocalizedTexts(notification, locale)
273 { 457 {
274 locale = locale || Utils.appLocale; 458 locale = locale || Utils.appLocale;
275 let textKeys = ["title", "message"]; 459 let textKeys = ["title", "message"];
276 let localizedTexts = []; 460 let localizedTexts = {};
277 for (let key of textKeys) 461 for (let key of textKeys)
278 { 462 {
279 if (key in notification) 463 if (key in notification)
280 { 464 {
281 if (typeof notification[key] == "string") 465 if (typeof notification[key] == "string")
282 localizedTexts[key] = notification[key]; 466 localizedTexts[key] = notification[key];
283 else 467 else
284 localizedTexts[key] = localize(notification[key], locale); 468 localizedTexts[key] = localize(notification[key], locale);
285 } 469 }
286 } 470 }
287 return localizedTexts; 471 return localizedTexts;
288 }, 472 },
289 473
290 /** 474 /**
291 * Adds a local notification. 475 * Adds a local notification.
292 * @param {Object} notification notification to add 476 * @param {Object} notification notification to add
293 */ 477 */
294 addNotification: function(notification) 478 addNotification(notification)
295 { 479 {
296 if (localData.indexOf(notification) == -1) 480 if (localData.indexOf(notification) == -1)
297 { 481 {
298 initNotificationMatcher(notification); 482 initNotificationMatcher(notification);
299 localData.push(notification); 483 localData.push(notification);
300 } 484 }
301 }, 485 },
302 486
303 /** 487 /**
304 * Removes an existing local notification. 488 * Removes an existing local notification.
305 * @param {Object} notification notification to remove 489 * @param {Object} notification notification to remove
306 */ 490 */
307 removeNotification: function(notification) 491 removeNotification(notification)
308 { 492 {
309 let index = localData.indexOf(notification); 493 let index = localData.indexOf(notification);
310 if (index > -1) 494 if (index > -1)
311 localData.splice(index, 1); 495 localData.splice(index, 1);
312 }, 496 },
313 497
314 /** 498 /**
499 * A callback function which listens to see if notifications were approved.
500 *
501 * @callback QuestionListener
502 * @param {boolean} approved
503 */
504
505 /**
315 * Adds a listener for question-type notifications 506 * Adds a listener for question-type notifications
316 */ 507 * @param {string} id
317 addQuestionListener: function(/**string*/ id, /**function(approved)*/ listener ) 508 * @param {QuestionListener} listener
318 { 509 */
319 if (!(id in listeners)) 510 addQuestionListener(id, listener)
320 listeners[id] = []; 511 {
321 if (listeners[id].indexOf(listener) === -1) 512 if (!(id in questionListeners))
322 listeners[id].push(listener); 513 questionListeners[id] = [];
514 if (questionListeners[id].indexOf(listener) === -1)
515 questionListeners[id].push(listener);
323 }, 516 },
324 517
325 /** 518 /**
326 * Removes a listener that was previously added via addQuestionListener 519 * Removes a listener that was previously added via addQuestionListener
327 */ 520 * @param {string} id
328 removeQuestionListener: function(/**string*/ id, /**function(approved)*/ liste ner) 521 * @param {QuestionListener} listener
329 { 522 */
330 if (!(id in listeners)) 523 removeQuestionListener(id, listener)
524 {
525 if (!(id in questionListeners))
331 return; 526 return;
332 let index = listeners[id].indexOf(listener); 527 let index = questionListeners[id].indexOf(listener);
333 if (index > -1) 528 if (index > -1)
334 listeners[id].splice(index, 1); 529 questionListeners[id].splice(index, 1);
335 if (listeners[id].length === 0) 530 if (questionListeners[id].length === 0)
336 delete listeners[id]; 531 delete questionListeners[id];
337 }, 532 },
338 533
339 /** 534 /**
340 * Notifies listeners about interactions with a notification 535 * Notifies question listeners about interactions with a notification
341 * @param {String} id notification ID 536 * @param {string} id notification ID
342 * @param {Boolean} approved indicator whether notification has been approved or not 537 * @param {boolean} approved indicator whether notification has been approved
343 */ 538 */
344 triggerQuestionListeners: function(id, approved) 539 triggerQuestionListeners(id, approved)
345 { 540 {
346 if (!(id in listeners)) 541 if (!(id in questionListeners))
347 return; 542 return;
348 let questionListeners = listeners[id]; 543 let listeners = questionListeners[id];
349 for (let listener of questionListeners) 544 for (let listener of listeners)
350 listener(approved); 545 listener(approved);
546 },
547
548 /**
549 * Toggles whether notifications of a specific category should be ignored
550 * @param {string} category notification category identifier
551 * @param {boolean} [forceValue] force specified value
552 */
553 toggleIgnoreCategory(category, forceValue)
554 {
555 let categories = Prefs.notifications_ignoredcategories;
556 let index = categories.indexOf(category);
557 if (index == -1 && forceValue !== false)
558 {
559 categories.push(category);
560 Prefs.notifications_showui = true;
561 }
562 else if (index != -1 && forceValue !== true)
563 categories.splice(index, 1);
564
565 // HACK: JSON values aren't saved unless they are assigned a
566 // different object.
567 Prefs.notifications_ignoredcategories =
568 JSON.parse(JSON.stringify(categories));
351 } 569 }
352 }; 570 };
353 Notification.init(); 571 Notification.init();
LEFTRIGHT
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Toggle Comments ('s')

Powered by Google App Engine
This is Rietveld