tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

NotificationBox.js (12582B)


      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 
      7 const {
      8  Component,
      9  createFactory,
     10 } = require("resource://devtools/client/shared/vendor/react.mjs");
     11 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
     12 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
     13 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     14 
     15 const l10n = new LocalizationHelper(
     16  "devtools/client/locales/components.properties"
     17 );
     18 const { div, span, button } = dom;
     19 loader.lazyGetter(this, "MDNLink", function () {
     20  return createFactory(
     21    require("resource://devtools/client/shared/components/MdnLink.js")
     22  );
     23 });
     24 
     25 // Priority Levels
     26 const PriorityLevels = {
     27  PRIORITY_INFO_LOW: 1,
     28  PRIORITY_INFO_MEDIUM: 2,
     29  PRIORITY_INFO_HIGH: 3,
     30  // Type NEW should be used to highlight new features, and should be more
     31  // eye-catchy than INFO level notifications.
     32  PRIORITY_NEW: 4,
     33  PRIORITY_WARNING_LOW: 5,
     34  PRIORITY_WARNING_MEDIUM: 6,
     35  PRIORITY_WARNING_HIGH: 7,
     36  PRIORITY_CRITICAL_LOW: 8,
     37  PRIORITY_CRITICAL_MEDIUM: 9,
     38  PRIORITY_CRITICAL_HIGH: 10,
     39  PRIORITY_CRITICAL_BLOCK: 11,
     40 };
     41 
     42 /**
     43 * This component represents Notification Box - HTML alternative for
     44 * <xul:notificationbox> binding.
     45 *
     46 * See also MDN for more info about <xul:notificationbox>:
     47 * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/notificationbox
     48 *
     49 * This component can maintain its own state (list of notifications)
     50 * as well as consume list of notifications provided as a prop
     51 * (coming e.g. from Redux store).
     52 */
     53 class NotificationBox extends Component {
     54  static get propTypes() {
     55    return {
     56      // Optional box ID (used for mounted node ID attribute)
     57      id: PropTypes.string,
     58      /**
     59       * List of notifications appended into the box. Each item of the map is an object
     60       * of the following shape:
     61       *   - {String} label: Label to appear on the notification.
     62       *   - {String} value: Value used to identify the notification. Should be the same
     63       *                     as the map key used for this notification.
     64       *   - {String} image: URL of image to appear on the notification. If "" then an
     65       *                     appropriate icon for the priority level is used.
     66       *   - {Number} priority: Notification priority; see Priority Levels.
     67       *   - {Function} eventCallback: A function to call to notify you of interesting
     68                                       things that happen with the notification box.
     69           - {String} type: One of "info", "warning", or "critical" used to determine
     70                            what styling and icon are used for the notification.
     71       *   - {Array<Object>} buttons: Array of button descriptions to appear on the
     72       *                              notification. Should be of the following shape:
     73       *                     - {Function} callback: This function is passed 3 arguments:
     74                                                    1) the NotificationBox component
     75                                                       the button is associated with.
     76                                                    2) the button description as passed
     77                                                       to appendNotification.
     78                                                    3) the element which was the target
     79                                                       of the button press event.
     80                                                    If the return value from this function
     81                                                    is not true, then the notification is
     82                                                    closed. The notification is also not
     83                                                    closed if an error is thrown.
     84                             - {String} label: The label to appear on the button.
     85                             - {String} accesskey: The accesskey attribute set on the
     86                                                   <button> element.
     87                             - {String} mdnUrl: URL to MDN docs. Optional but if set
     88                                                turns button into a MDNLink and supersedes
     89                                                all other properties. Uses Label as the title
     90                                                for the link.
     91      */
     92      notifications: PropTypes.instanceOf(Map),
     93      // Message that should be shown when hovering over the close button
     94      closeButtonTooltip: PropTypes.string,
     95      // Wraps text when passed from console window as wrapping: true
     96      wrapping: PropTypes.bool,
     97      // Display a top border (default to false)
     98      displayBorderTop: PropTypes.bool,
     99      // Display a bottom border (default to true)
    100      displayBorderBottom: PropTypes.bool,
    101      // Display a close button (default to true)
    102      displayCloseButton: PropTypes.bool,
    103    };
    104  }
    105 
    106  static get defaultProps() {
    107    return {
    108      closeButtonTooltip: l10n.getStr("notificationBox.closeTooltip"),
    109      displayBorderTop: false,
    110      displayBorderBottom: true,
    111      displayCloseButton: true,
    112    };
    113  }
    114 
    115  constructor(props) {
    116    super(props);
    117 
    118    this.state = {
    119      notifications: new Map(),
    120    };
    121 
    122    this.appendNotification = this.appendNotification.bind(this);
    123    this.removeNotification = this.removeNotification.bind(this);
    124    this.getNotificationWithValue = this.getNotificationWithValue.bind(this);
    125    this.getCurrentNotification = this.getCurrentNotification.bind(this);
    126    this.close = this.close.bind(this);
    127    this.renderButton = this.renderButton.bind(this);
    128    this.renderNotification = this.renderNotification.bind(this);
    129  }
    130 
    131  /**
    132   * Create a new notification and display it. If another notification is
    133   * already present with a higher priority, the new notification will be
    134   * added behind it. See `propTypes` for arguments description.
    135   */
    136  appendNotification(
    137    label,
    138    value,
    139    image,
    140    priority,
    141    buttons = [],
    142    eventCallback
    143  ) {
    144    const newState = appendNotification(this.state, {
    145      label,
    146      value,
    147      image,
    148      priority,
    149      buttons,
    150      eventCallback,
    151    });
    152 
    153    this.setState(newState);
    154  }
    155 
    156  /**
    157   * Remove specific notification from the list.
    158   */
    159  removeNotification(notification) {
    160    if (notification) {
    161      this.close(this.state.notifications.get(notification.value));
    162    }
    163  }
    164 
    165  /**
    166   * Returns an object that represents a notification. It can be
    167   * used to close it.
    168   */
    169  getNotificationWithValue(value) {
    170    const notification = this.state.notifications.get(value);
    171    if (!notification) {
    172      return null;
    173    }
    174 
    175    // Return an object that can be used to remove the notification
    176    // later (using `removeNotification` method) or directly close it.
    177    return Object.assign({}, notification, {
    178      close: () => {
    179        this.close(notification);
    180      },
    181    });
    182  }
    183 
    184  getCurrentNotification() {
    185    return getHighestPriorityNotification(this.state.notifications);
    186  }
    187 
    188  /**
    189   * Close specified notification.
    190   */
    191  close(notification) {
    192    if (!notification) {
    193      return;
    194    }
    195 
    196    if (notification.eventCallback) {
    197      notification.eventCallback("removed");
    198    }
    199 
    200    if (!this.state.notifications.get(notification.value)) {
    201      return;
    202    }
    203 
    204    const newNotifications = new Map(this.state.notifications);
    205    newNotifications.delete(notification.value);
    206    this.setState({
    207      notifications: newNotifications,
    208    });
    209  }
    210 
    211  /**
    212   * Render a button. A notification can have a set of custom buttons.
    213   * These are used to execute custom callback. Will render a MDNLink
    214   * if mdnUrl property is set.
    215   */
    216  renderButton(props, notification) {
    217    if (props.mdnUrl != null) {
    218      return MDNLink({
    219        url: props.mdnUrl,
    220        title: props.label,
    221      });
    222    }
    223    const onClick = event => {
    224      if (props.callback) {
    225        const result = props.callback(this, props, event.target);
    226        if (!result) {
    227          this.close(notification);
    228        }
    229        event.stopPropagation();
    230      }
    231    };
    232 
    233    return button(
    234      {
    235        key: props.label,
    236        className: "notificationButton",
    237        accesskey: props.accesskey,
    238        onClick,
    239      },
    240      props.label
    241    );
    242  }
    243 
    244  /**
    245   * Render a notification.
    246   */
    247  renderNotification(notification) {
    248    return div(
    249      {
    250        key: notification.value,
    251        className: "notification",
    252        "data-key": notification.value,
    253        "data-type": notification.type,
    254      },
    255      div(
    256        { className: "notificationInner" },
    257        div({
    258          className: "messageImage",
    259          "data-type": notification.type,
    260        }),
    261        span(
    262          {
    263            className: "messageText",
    264            title: notification.label,
    265          },
    266          notification.label
    267        ),
    268        notification.buttons.map(props =>
    269          this.renderButton(props, notification)
    270        ),
    271        this.props.displayCloseButton
    272          ? button({
    273              className: "messageCloseButton",
    274              title: this.props.closeButtonTooltip,
    275              onClick: this.close.bind(this, notification),
    276            })
    277          : null
    278      )
    279    );
    280  }
    281 
    282  /**
    283   * Render the top (highest priority) notification. Only one
    284   * notification is rendered at a time.
    285   */
    286  render() {
    287    const notifications = this.props.notifications || this.state.notifications;
    288    const notification = getHighestPriorityNotification(notifications);
    289    const content = notification ? this.renderNotification(notification) : null;
    290 
    291    const classNames = ["notificationbox"];
    292    if (this.props.wrapping) {
    293      classNames.push("wrapping");
    294    }
    295 
    296    if (this.props.displayBorderBottom) {
    297      classNames.push("border-bottom");
    298    }
    299 
    300    if (this.props.displayBorderTop) {
    301      classNames.push("border-top");
    302    }
    303 
    304    return div(
    305      {
    306        className: classNames.join(" "),
    307        id: this.props.id,
    308      },
    309      content
    310    );
    311  }
    312 }
    313 
    314 // Helpers
    315 
    316 /**
    317 * Create a new notification. If another notification is already present with
    318 * a higher priority, the new notification will be added behind it.
    319 * See `propTypes` for arguments description.
    320 */
    321 function appendNotification(state, props) {
    322  const { label, value, image, priority, buttons, eventCallback } = props;
    323 
    324  // Priority level must be within expected interval
    325  // (see priority levels at the top of this file).
    326  if (
    327    priority < PriorityLevels.PRIORITY_INFO_LOW ||
    328    priority > PriorityLevels.PRIORITY_CRITICAL_BLOCK
    329  ) {
    330    throw new Error("Invalid notification priority " + priority);
    331  }
    332 
    333  // Custom image URL is not supported yet.
    334  if (image) {
    335    throw new Error("Custom image URL is not supported yet");
    336  }
    337 
    338  let type = "warning";
    339  if (priority == PriorityLevels.PRIORITY_NEW) {
    340    type = "new";
    341  } else if (priority >= PriorityLevels.PRIORITY_CRITICAL_LOW) {
    342    type = "critical";
    343  } else if (priority <= PriorityLevels.PRIORITY_INFO_HIGH) {
    344    type = "info";
    345  }
    346 
    347  if (!state.notifications) {
    348    state.notifications = new Map();
    349  }
    350 
    351  const notifications = new Map(state.notifications);
    352  notifications.set(value, {
    353    label,
    354    value,
    355    image,
    356    priority,
    357    type,
    358    buttons: Array.isArray(buttons) ? buttons : [],
    359    eventCallback,
    360  });
    361 
    362  return {
    363    notifications,
    364  };
    365 }
    366 
    367 function getNotificationWithValue(notifications, value) {
    368  return notifications ? notifications.get(value) : null;
    369 }
    370 
    371 function removeNotificationWithValue(notifications, value) {
    372  const newNotifications = new Map(notifications);
    373  newNotifications.delete(value);
    374 
    375  return {
    376    notifications: newNotifications,
    377  };
    378 }
    379 
    380 function getHighestPriorityNotification(notifications) {
    381  if (!notifications) {
    382    return null;
    383  }
    384 
    385  let currentNotification = null;
    386  // High priorities must be on top.
    387  for (const [, notification] of notifications) {
    388    if (
    389      !currentNotification ||
    390      notification.priority > currentNotification.priority
    391    ) {
    392      currentNotification = notification;
    393    }
    394  }
    395 
    396  return currentNotification;
    397 }
    398 
    399 module.exports.NotificationBox = NotificationBox;
    400 module.exports.PriorityLevels = PriorityLevels;
    401 module.exports.appendNotification = appendNotification;
    402 module.exports.getNotificationWithValue = getNotificationWithValue;
    403 module.exports.removeNotificationWithValue = removeNotificationWithValue;