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