tor-browser

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

UrlbarProviderQuickSuggestContextualOptIn.sys.mjs (9373B)


      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 /**
      6 * This module exports a provider that offers a search engine when the user is
      7 * typing a search engine domain.
      8 */
      9 
     10 import {
     11  UrlbarProvider,
     12  UrlbarUtils,
     13 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs";
     14 
     15 const lazy = {};
     16 
     17 ChromeUtils.defineESModuleGetters(lazy, {
     18  UrlbarView: "moz-src:///browser/components/urlbar/UrlbarView.sys.mjs",
     19  UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs",
     20  UrlbarProviderTopSites:
     21    "moz-src:///browser/components/urlbar/UrlbarProviderTopSites.sys.mjs",
     22  UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs",
     23 });
     24 
     25 const DYNAMIC_RESULT_TYPE = "quickSuggestContextualOptIn";
     26 const VIEW_TEMPLATE = {
     27  children: [
     28    {
     29      name: "no-wrap",
     30      tag: "span",
     31      classList: ["urlbarView-no-wrap"],
     32      children: [
     33        {
     34          name: "icon",
     35          tag: "img",
     36          classList: ["urlbarView-favicon"],
     37        },
     38        {
     39          name: "text-container",
     40          tag: "span",
     41          children: [
     42            {
     43              name: "title",
     44              tag: "strong",
     45            },
     46            {
     47              name: "description",
     48              tag: "span",
     49              children: [
     50                {
     51                  name: "learn_more",
     52                  tag: "a",
     53                  attributes: {
     54                    "data-l10n-name": "learn-more-link",
     55                    selectable: true,
     56                  },
     57                },
     58              ],
     59            },
     60          ],
     61        },
     62      ],
     63    },
     64  ],
     65 };
     66 
     67 /**
     68 * Initializes this provider's dynamic result. To be called after the creation
     69 * of the provider singleton.
     70 */
     71 function initializeDynamicResult() {
     72  lazy.UrlbarResult.addDynamicResultType(DYNAMIC_RESULT_TYPE);
     73  lazy.UrlbarView.addDynamicViewTemplate(DYNAMIC_RESULT_TYPE, VIEW_TEMPLATE);
     74 }
     75 
     76 /**
     77 * Class used to create the provider.
     78 */
     79 export class UrlbarProviderQuickSuggestContextualOptIn extends UrlbarProvider {
     80  constructor() {
     81    super();
     82  }
     83 
     84  /**
     85   * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>}
     86   */
     87  get type() {
     88    return UrlbarUtils.PROVIDER_TYPE.HEURISTIC;
     89  }
     90 
     91  #shouldDisplayContextualOptIn(queryContext = null) {
     92    if (
     93      queryContext &&
     94      (queryContext.isPrivate ||
     95        queryContext.restrictSource ||
     96        queryContext.searchString ||
     97        queryContext.searchMode)
     98    ) {
     99      return false;
    100    }
    101 
    102    // If the feature is disabled, or the user has already opted in, don't show
    103    // the onboarding.
    104    if (
    105      !lazy.UrlbarPrefs.get("quickSuggestEnabled") ||
    106      !lazy.UrlbarPrefs.get("quicksuggest.contextualOptIn") ||
    107      lazy.UrlbarPrefs.get("quicksuggest.online.enabled")
    108    ) {
    109      return false;
    110    }
    111 
    112    let lastDismissedTime = lazy.UrlbarPrefs.get(
    113      "quicksuggest.contextualOptIn.lastDismissedTime"
    114    );
    115    if (!lastDismissedTime) {
    116      return true;
    117    }
    118 
    119    let dismissedCount = lazy.UrlbarPrefs.get(
    120      "quicksuggest.contextualOptIn.dismissedCount"
    121    );
    122 
    123    let reshowAfterPeriodDays;
    124    switch (dismissedCount) {
    125      case 1: {
    126        reshowAfterPeriodDays = lazy.UrlbarPrefs.get(
    127          "quicksuggest.contextualOptIn.firstReshowAfterPeriodDays"
    128        );
    129        break;
    130      }
    131      case 2: {
    132        reshowAfterPeriodDays = lazy.UrlbarPrefs.get(
    133          "quicksuggest.contextualOptIn.secondReshowAfterPeriodDays"
    134        );
    135        break;
    136      }
    137      case 3: {
    138        reshowAfterPeriodDays = lazy.UrlbarPrefs.get(
    139          "quicksuggest.contextualOptIn.thirdReshowAfterPeriodDays"
    140        );
    141        break;
    142      }
    143      default: {
    144        return false;
    145      }
    146    }
    147 
    148    let time = reshowAfterPeriodDays * 24 * 60 * 60;
    149    return Date.now() / 1000 - lastDismissedTime > time;
    150  }
    151 
    152  async isActive(queryContext) {
    153    if (!this.#shouldDisplayContextualOptIn(queryContext)) {
    154      return false;
    155    }
    156 
    157    // Evaluate impressions in order to dismiss.
    158    let firstImpressionTime = lazy.UrlbarPrefs.get(
    159      "quicksuggest.contextualOptIn.firstImpressionTime"
    160    );
    161    if (!firstImpressionTime) {
    162      return true;
    163    }
    164 
    165    let impressionCount = lazy.UrlbarPrefs.get(
    166      "quicksuggest.contextualOptIn.impressionCount"
    167    );
    168    let impressionLimit = lazy.UrlbarPrefs.get(
    169      "quicksuggest.contextualOptIn.impressionLimit"
    170    );
    171 
    172    if (impressionCount < impressionLimit) {
    173      return true;
    174    }
    175 
    176    let daysLimit = lazy.UrlbarPrefs.get(
    177      "quicksuggest.contextualOptIn.impressionDaysLimit"
    178    );
    179    let timeLimit = daysLimit * 24 * 60 * 60;
    180    if (Date.now() / 1000 - firstImpressionTime < timeLimit) {
    181      return true;
    182    }
    183 
    184    this.#dismiss();
    185 
    186    return false;
    187  }
    188 
    189  getPriority() {
    190    return lazy.UrlbarProviderTopSites.PRIORITY;
    191  }
    192 
    193  /**
    194   * This is called only for dynamic result types, when the urlbar view updates
    195   * the view of one of the results of the provider.  It should return an object
    196   * describing the view update.
    197   *
    198   * @returns {object} An object describing the view update.
    199   */
    200  getViewUpdate() {
    201    return {
    202      icon: {
    203        attributes: {
    204          src: "chrome://branding/content/icon32.png",
    205        },
    206      },
    207      title: {
    208        l10n: {
    209          id: "urlbar-firefox-suggest-contextual-opt-in-title-1",
    210        },
    211      },
    212      description: {
    213        l10n: {
    214          id: "urlbar-firefox-suggest-contextual-opt-in-description-3",
    215        },
    216      },
    217    };
    218  }
    219 
    220  onBeforeSelection(result, element) {
    221    if (element.getAttribute("name") == "learn_more") {
    222      this.#a11yAlertRow(element.closest(".urlbarView-row"));
    223    }
    224  }
    225 
    226  #a11yAlertRow(row) {
    227    let alertText = row.querySelector(
    228      ".urlbarView-dynamic-quickSuggestContextualOptIn-title"
    229    ).textContent;
    230    let decription = row
    231      .querySelector(
    232        ".urlbarView-dynamic-quickSuggestContextualOptIn-description"
    233      )
    234      .cloneNode(true);
    235    // Remove the "Learn More" link.
    236    decription.firstElementChild?.remove();
    237    alertText += ". " + decription.textContent;
    238    row.ownerGlobal.A11yUtils.announce({ raw: alertText });
    239  }
    240 
    241  onImpression(state, _queryContext, _controller, _resultsAndIndexes, details) {
    242    if (state == "engagement" && details.provider == this.name) {
    243      return;
    244    }
    245 
    246    let impressionCount = lazy.UrlbarPrefs.get(
    247      "quicksuggest.contextualOptIn.impressionCount"
    248    );
    249    lazy.UrlbarPrefs.set(
    250      "quicksuggest.contextualOptIn.impressionCount",
    251      impressionCount + 1
    252    );
    253 
    254    let firstImpressionTime = lazy.UrlbarPrefs.get(
    255      "quicksuggest.contextualOptIn.firstImpressionTime"
    256    );
    257    if (!firstImpressionTime) {
    258      lazy.UrlbarPrefs.set(
    259        "quicksuggest.contextualOptIn.firstImpressionTime",
    260        Date.now() / 1000
    261      );
    262    }
    263  }
    264 
    265  onEngagement(queryContext, controller, details) {
    266    this._handleCommand(details.element, controller, details.result);
    267  }
    268 
    269  _handleCommand(element, controller, result, container) {
    270    let commandName = element?.getAttribute("name");
    271    switch (commandName) {
    272      case "learn_more":
    273        controller.browserWindow.openHelpLink("firefox-suggest");
    274        break;
    275      case "allow":
    276        lazy.UrlbarPrefs.set("quicksuggest.online.enabled", true);
    277        break;
    278      case "dismiss":
    279        this.#dismiss();
    280        break;
    281      default:
    282        return;
    283    }
    284 
    285    // Remove the result if it shouldn't be active anymore due to above
    286    // actions.
    287    if (!this.#shouldDisplayContextualOptIn()) {
    288      if (result) {
    289        controller.removeResult(result);
    290      } else {
    291        // This is for when the UI is outside of standard results, after
    292        // one-off search buttons.
    293        container.hidden = true;
    294      }
    295    }
    296  }
    297 
    298  #dismiss() {
    299    lazy.UrlbarPrefs.set("quicksuggest.contextualOptIn.firstImpressionTime", 0);
    300    lazy.UrlbarPrefs.set("quicksuggest.contextualOptIn.impressionCount", 0);
    301 
    302    lazy.UrlbarPrefs.set(
    303      "quicksuggest.contextualOptIn.lastDismissedTime",
    304      Date.now() / 1000
    305    );
    306    let dismissedCount = lazy.UrlbarPrefs.get(
    307      "quicksuggest.contextualOptIn.dismissedCount"
    308    );
    309    lazy.UrlbarPrefs.set(
    310      "quicksuggest.contextualOptIn.dismissedCount",
    311      dismissedCount + 1
    312    );
    313  }
    314 
    315  /**
    316   * Starts querying.
    317   *
    318   * @param {UrlbarQueryContext} queryContext
    319   * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback
    320   *   Callback invoked by the provider to add a new result.
    321   */
    322  async startQuery(queryContext, addCallback) {
    323    let result = new lazy.UrlbarResult({
    324      type: UrlbarUtils.RESULT_TYPE.DYNAMIC,
    325      source: UrlbarUtils.RESULT_SOURCE.SEARCH,
    326      suggestedIndex: 0,
    327      payload: {
    328        buttons: [
    329          {
    330            l10n: {
    331              id: "urlbar-firefox-suggest-contextual-opt-in-allow",
    332            },
    333            attributes: { primary: true, name: "allow" },
    334          },
    335          {
    336            l10n: {
    337              id: "urlbar-firefox-suggest-contextual-opt-in-dismiss",
    338            },
    339            attributes: { name: "dismiss" },
    340          },
    341        ],
    342        dynamicType: DYNAMIC_RESULT_TYPE,
    343      },
    344    });
    345    addCallback(this, result);
    346  }
    347 }
    348 
    349 initializeDynamicResult();