tor-browser

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

ImportantDatesSuggestions.sys.mjs (7895B)


      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
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { SuggestProvider } from "moz-src:///browser/components/urlbar/private/SuggestFeature.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  QuickSuggest: "moz-src:///browser/components/urlbar/QuickSuggest.sys.mjs",
     11  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     12  UrlbarSearchUtils:
     13    "moz-src:///browser/components/urlbar/UrlbarSearchUtils.sys.mjs",
     14  UrlbarUtils: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs",
     15 });
     16 
     17 const SHOW_COUNTDOWN_THRESHOLD_DAYS = 30;
     18 const MS_PER_DAY = 1000 * 60 * 60 * 24;
     19 
     20 /**
     21 * A Suggest feature that manages important-dates suggestions.
     22 */
     23 export class ImportantDatesSuggestions extends SuggestProvider {
     24  get enablingPreferences() {
     25    return ["importantDatesFeatureGate", "suggest.importantDates"];
     26  }
     27 
     28  get primaryUserControlledPreferences() {
     29    return ["suggest.importantDates"];
     30  }
     31 
     32  get rustSuggestionType() {
     33    return "Dynamic";
     34  }
     35 
     36  get dynamicRustSuggestionTypes() {
     37    return ["important_dates"];
     38  }
     39 
     40  isSuggestionSponsored(suggestion) {
     41    if (suggestion.data?.result?.payload?.hasOwnProperty("isSponsored")) {
     42      return suggestion.data.result.payload.isSponsored;
     43    }
     44    return false;
     45  }
     46 
     47  getSuggestionTelemetryType(suggestion) {
     48    if (suggestion.data?.result?.payload?.hasOwnProperty("telemetryType")) {
     49      return suggestion.data.result.payload.telemetryType;
     50    }
     51    return this.dynamicRustSuggestionTypes[0];
     52  }
     53 
     54  async makeResult(queryContext, suggestion, _searchString) {
     55    if (
     56      !suggestion.data?.result?.payload ||
     57      typeof suggestion.data.result.payload != "object"
     58    ) {
     59      this.logger.warn("Unexpected remote settings suggestion");
     60      return null;
     61    }
     62 
     63    return this.#makeDateResult(queryContext, suggestion.data.result.payload);
     64  }
     65 
     66  /**
     67   * @typedef DatePayload
     68   *   The shape of an important dates suggestion payload.
     69   * @property {(string | [string, string])[]} dates
     70   *   An array of dates or date tuples in the format "YYYY-MM-DD".
     71   *   For a date tuple, the first value represents the start and the second
     72   *   the end date. This is assumed to be ordered oldest to newest.
     73   * @property {string} name
     74   *   The name of the event.
     75   */
     76 
     77  /**
     78   * Returns a string about the date of an event that can be displayed.
     79   *
     80   * @param {string | [string, string]} dateStr
     81   *   A string in the format "YYYY-MM-DD" representing a single
     82   *   day or a tuple of strings representing start and end days.
     83   * @returns {string}
     84   *   A localized string about the date of the event.
     85   */
     86  #formatDateOrRange(dateStr) {
     87    if (Array.isArray(dateStr)) {
     88      let format = new Intl.DateTimeFormat(Services.locale.appLocaleAsBCP47, {
     89        year: "numeric",
     90        month: "long",
     91        day: "numeric",
     92      });
     93      let startDate = new Date(dateStr[0] + "T00:00");
     94      let endDate = new Date(dateStr[1] + "T00:00");
     95      return format.formatRange(startDate, endDate);
     96    }
     97 
     98    let format = new Intl.DateTimeFormat(Services.locale.appLocaleAsBCP47, {
     99      weekday: "long",
    100      year: "numeric",
    101      month: "long",
    102      day: "numeric",
    103    });
    104    let date = new Date(dateStr + "T00:00");
    105    return format.format(date);
    106  }
    107 
    108  /**
    109   * Returns a l10n object about the name of and time until an event
    110   * that can be used as the description of a urlbar result.
    111   *
    112   * @param {string | [string, string]} dateStr
    113   *   A string in the format "YYYY-MM-DD" representing a single
    114   *   day or a tuple of strings representing start and end days.
    115   * @param {string} name
    116   *   The name of the event.
    117   * @returns {?object}
    118   *   A l10n object or null if the event is in the past.
    119   */
    120  #formatDateCountdown(dateStr, name) {
    121    if (Array.isArray(dateStr)) {
    122      let daysUntilStart = this.#getDaysUntil(dateStr[0]);
    123      let daysUntilEnd = this.#getDaysUntil(dateStr[1]);
    124      if (daysUntilEnd < 0) {
    125        throw new Error("Date lies in the past.");
    126      }
    127      if (daysUntilStart > 0) {
    128        return {
    129          id: "urlbar-result-dates-countdown-range",
    130          args: { daysUntilStart, name },
    131        };
    132      }
    133      if (daysUntilEnd > 0) {
    134        return {
    135          id: "urlbar-result-dates-ongoing",
    136          args: { daysUntilEnd, name },
    137        };
    138      }
    139      return {
    140        id: "urlbar-result-dates-ends-today",
    141        args: { name },
    142      };
    143    }
    144 
    145    let daysUntil = this.#getDaysUntil(dateStr);
    146    if (daysUntil < 0) {
    147      throw new Error("Date lies in the past.");
    148    }
    149    if (daysUntil > 0) {
    150      return {
    151        id: "urlbar-result-dates-countdown",
    152        args: { daysUntilStart: daysUntil, name },
    153      };
    154    }
    155    return {
    156      id: "urlbar-result-dates-today",
    157      args: { name },
    158    };
    159  }
    160 
    161  /**
    162   * Returns the number of days until the specified date.
    163   *
    164   * @param {string} dateStr
    165   *   A string in the format "YYYY-MM-DD".
    166   * @returns {number}
    167   *   The time until the input date in days.
    168   */
    169  #getDaysUntil(dateStr) {
    170    let now = new Date();
    171    now.setHours(0, 0, 0, 0);
    172    let date = new Date(dateStr + "T00:00");
    173 
    174    let msUntil = date.getTime() - now.getTime();
    175    // Round to account for potential DST.
    176    return Math.round(msUntil / MS_PER_DAY);
    177  }
    178 
    179  /**
    180   * Creates a urlbar result from an important dates payload.
    181   *
    182   * @param {UrlbarQueryContext} queryContext
    183   *   The query context.
    184   * @param {DatePayload} payload
    185   *   The important dates payload.
    186   * @returns {?UrlbarResult}
    187   *   A urlbar result or null if all instances are in the past.
    188   */
    189  #makeDateResult(queryContext, payload) {
    190    let eventDateOrRange = payload.dates.find(
    191      // Find first entry where the end date is in the future.
    192      // This is always the upcoming occurrence since dates is ordered by time.
    193      d => this.#getDaysUntil(Array.isArray(d) ? d[1] : d) >= 0
    194    );
    195    if (!eventDateOrRange) {
    196      // All instances of the event are in the past.
    197      return null;
    198    }
    199 
    200    let daysUntilStart = this.#getDaysUntil(
    201      Array.isArray(eventDateOrRange) ? eventDateOrRange[0] : eventDateOrRange
    202    );
    203 
    204    let description, descriptionL10n;
    205    if (daysUntilStart > SHOW_COUNTDOWN_THRESHOLD_DAYS) {
    206      description = payload.name;
    207    } else {
    208      descriptionL10n = {
    209        ...this.#formatDateCountdown(eventDateOrRange, payload.name),
    210      };
    211    }
    212 
    213    let dateString = this.#formatDateOrRange(eventDateOrRange);
    214    return new lazy.UrlbarResult({
    215      type: lazy.UrlbarUtils.RESULT_TYPE.SEARCH,
    216      source: lazy.UrlbarUtils.RESULT_SOURCE.SEARCH,
    217      isBestMatch: true,
    218      hideRowLabel: true,
    219      richSuggestionIconSize: 24,
    220      payload: {
    221        title: dateString,
    222        description,
    223        engine: lazy.UrlbarSearchUtils.getDefaultEngine(queryContext.isPrivate)
    224          .name,
    225        descriptionL10n,
    226        query: payload.name,
    227        lowerCaseSuggestion: payload.name.toLowerCase(),
    228        icon: "chrome://browser/skin/calendar-24.svg",
    229        helpUrl: lazy.QuickSuggest.HELP_URL,
    230        isManageable: true,
    231        isBlockable: true,
    232      },
    233      highlights: {
    234        title: lazy.UrlbarUtils.HIGHLIGHT.ALL,
    235      },
    236    });
    237  }
    238 
    239  onEngagement(_queryContext, controller, details, _searchString) {
    240    switch (details.selType) {
    241      case "manage":
    242        // "manage" is handled by UrlbarInput, no need to do anything here.
    243        break;
    244      case "dismiss": {
    245        let { result } = details;
    246        lazy.QuickSuggest.dismissResult(result);
    247        result.acknowledgeDismissalL10n = {
    248          id: "firefox-suggest-dismissal-acknowledgment-one",
    249        };
    250        controller.removeResult(result);
    251        break;
    252      }
    253    }
    254  }
    255 }