1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7var EXPORTED_SYMBOLS = ["AppMenuNotifications"]; 8 9const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 10 11function AppMenuNotification(id, mainAction, secondaryAction, options = {}) { 12 this.id = id; 13 this.mainAction = mainAction; 14 this.secondaryAction = secondaryAction; 15 this.options = options; 16 this.dismissed = this.options.dismissed || false; 17} 18 19var AppMenuNotifications = { 20 _notifications: [], 21 _hasInitialized: false, 22 23 get notifications() { 24 return Array.from(this._notifications); 25 }, 26 27 _lazyInit() { 28 if (!this._hasInitialized) { 29 Services.obs.addObserver(this, "xpcom-shutdown"); 30 Services.obs.addObserver(this, "appMenu-notifications-request"); 31 } 32 }, 33 34 uninit() { 35 Services.obs.removeObserver(this, "xpcom-shutdown"); 36 Services.obs.removeObserver(this, "appMenu-notifications-request"); 37 }, 38 39 observe(subject, topic, status) { 40 switch (topic) { 41 case "xpcom-shutdown": 42 this.uninit(); 43 break; 44 case "appMenu-notifications-request": 45 if (this._notifications.length) { 46 Services.obs.notifyObservers(null, "appMenu-notifications", "init"); 47 } 48 break; 49 } 50 }, 51 52 get activeNotification() { 53 if (this._notifications.length) { 54 const doorhanger = this._notifications.find( 55 n => !n.dismissed && !n.options.badgeOnly 56 ); 57 return doorhanger || this._notifications[0]; 58 } 59 60 return null; 61 }, 62 63 showNotification(id, mainAction, secondaryAction, options = {}) { 64 let notification = new AppMenuNotification( 65 id, 66 mainAction, 67 secondaryAction, 68 options 69 ); 70 let existingIndex = this._notifications.findIndex(n => n.id == id); 71 if (existingIndex != -1) { 72 this._notifications.splice(existingIndex, 1); 73 } 74 75 // We don't want to clobber doorhanger notifications just to show a badge, 76 // so don't dismiss any of them and the badge will show once the doorhanger 77 // gets resolved. 78 if (!options.badgeOnly && !options.dismissed) { 79 this._notifications.forEach(n => { 80 n.dismissed = true; 81 }); 82 } 83 84 // Since notifications are generally somewhat pressing, the ideal case is that 85 // we never have two notifications at once. However, in the event that we do, 86 // it's more likely that the older notification has been sitting around for a 87 // bit, and so we don't want to hide the new notification behind it. Thus, 88 // we want our notifications to behave like a stack instead of a queue. 89 this._notifications.unshift(notification); 90 91 this._lazyInit(); 92 this._updateNotifications(); 93 return notification; 94 }, 95 96 showBadgeOnlyNotification(id) { 97 return this.showNotification(id, null, null, { badgeOnly: true }); 98 }, 99 100 removeNotification(id) { 101 let notifications; 102 if (typeof id == "string") { 103 notifications = this._notifications.filter(n => n.id == id); 104 } else { 105 // If it's not a string, assume RegExp 106 notifications = this._notifications.filter(n => id.test(n.id)); 107 } 108 // _updateNotifications can be expensive if it forces attachment of XBL 109 // bindings that haven't been used yet, so return early if we haven't found 110 // any notification to remove, as callers may expect this removeNotification 111 // method to be a no-op for non-existent notifications. 112 if (!notifications.length) { 113 return; 114 } 115 116 notifications.forEach(n => { 117 this._removeNotification(n); 118 }); 119 this._updateNotifications(); 120 }, 121 122 dismissNotification(id) { 123 let notifications; 124 if (typeof id == "string") { 125 notifications = this._notifications.filter(n => n.id == id); 126 } else { 127 // If it's not a string, assume RegExp 128 notifications = this._notifications.filter(n => id.test(n.id)); 129 } 130 131 notifications.forEach(n => { 132 n.dismissed = true; 133 if (n.options.onDismissed) { 134 n.options.onDismissed(); 135 } 136 }); 137 this._updateNotifications(); 138 }, 139 140 callMainAction(win, notification, fromDoorhanger) { 141 let action = notification.mainAction; 142 this._callAction(win, notification, action, fromDoorhanger); 143 }, 144 145 callSecondaryAction(win, notification) { 146 let action = notification.secondaryAction; 147 this._callAction(win, notification, action, true); 148 }, 149 150 _callAction(win, notification, action, fromDoorhanger) { 151 let dismiss = true; 152 if (action) { 153 try { 154 action.callback(win, fromDoorhanger); 155 } catch (error) { 156 Cu.reportError(error); 157 } 158 159 dismiss = action.dismiss; 160 } 161 162 if (dismiss) { 163 notification.dismissed = true; 164 } else { 165 this._removeNotification(notification); 166 } 167 168 this._updateNotifications(); 169 }, 170 171 _removeNotification(notification) { 172 // This notification may already be removed, in which case let's just ignore. 173 let notifications = this._notifications; 174 if (!notifications) { 175 return; 176 } 177 178 var index = notifications.indexOf(notification); 179 if (index == -1) { 180 return; 181 } 182 183 // Remove the notification 184 notifications.splice(index, 1); 185 }, 186 187 _updateNotifications() { 188 Services.obs.notifyObservers(null, "appMenu-notifications", "update"); 189 }, 190}; 191