tor-browser

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

history.mjs (15436B)


      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 {
      6  html,
      7  ifDefined,
      8  when,
      9 } from "chrome://global/content/vendor/lit.all.mjs";
     10 import { escapeHtmlEntities, navigateToLink } from "./helpers.mjs";
     11 import { ViewPage } from "./viewpage.mjs";
     12 // eslint-disable-next-line import/no-unassigned-import
     13 import "chrome://browser/content/migration/migration-wizard.mjs";
     14 // eslint-disable-next-line import/no-unassigned-import
     15 import "chrome://global/content/elements/moz-button.mjs";
     16 
     17 const lazy = {};
     18 
     19 ChromeUtils.defineESModuleGetters(lazy, {
     20  HistoryController: "resource:///modules/HistoryController.sys.mjs",
     21  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     22  ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
     23 });
     24 
     25 let XPCOMUtils = ChromeUtils.importESModule(
     26  "resource://gre/modules/XPCOMUtils.sys.mjs"
     27 ).XPCOMUtils;
     28 
     29 const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
     30 const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history";
     31 const IMPORT_HISTORY_DISMISSED_PREF =
     32  "browser.tabs.firefox-view.importHistory.dismissed";
     33 
     34 const SEARCH_RESULTS_LIMIT = 300;
     35 
     36 class HistoryInView extends ViewPage {
     37  constructor() {
     38    super();
     39    this._started = false;
     40    // Setting maxTabsLength to -1 for no max
     41    this.maxTabsLength = -1;
     42    this.profileAge = 8;
     43    this.fullyUpdated = false;
     44    this.cumulativeSearches = 0;
     45  }
     46 
     47  controller = new lazy.HistoryController(this, {
     48    searchResultsLimit: SEARCH_RESULTS_LIMIT,
     49  });
     50 
     51  start() {
     52    if (this._started) {
     53      return;
     54    }
     55    this._started = true;
     56 
     57    this.controller.updateCache();
     58 
     59    this.toggleVisibilityInCardContainer();
     60  }
     61 
     62  async connectedCallback() {
     63    super.connectedCallback();
     64    XPCOMUtils.defineLazyPreferenceGetter(
     65      this,
     66      "importHistoryDismissedPref",
     67      IMPORT_HISTORY_DISMISSED_PREF,
     68      false,
     69      () => {
     70        this.requestUpdate();
     71      }
     72    );
     73    XPCOMUtils.defineLazyPreferenceGetter(
     74      this,
     75      "hasImportedHistoryPref",
     76      HAS_IMPORTED_HISTORY_PREF,
     77      false,
     78      () => {
     79        this.requestUpdate();
     80      }
     81    );
     82 
     83    if (!this.importHistoryDismissedPref && !this.hasImportedHistoryPrefs) {
     84      let profileAccessor = await lazy.ProfileAge();
     85      let profileCreateTime = await profileAccessor.created;
     86      let timeNow = new Date().getTime();
     87      let profileAge = timeNow - profileCreateTime;
     88      // Convert milliseconds to days
     89      this.profileAge = profileAge / 1000 / 60 / 60 / 24;
     90    }
     91  }
     92 
     93  stop() {
     94    if (!this._started) {
     95      return;
     96    }
     97    this._started = false;
     98 
     99    this.toggleVisibilityInCardContainer();
    100  }
    101 
    102  disconnectedCallback() {
    103    super.disconnectedCallback();
    104    this.stop();
    105    this.migrationWizardDialog?.removeEventListener(
    106      "MigrationWizard:Close",
    107      this.migrationWizardDialog
    108    );
    109  }
    110 
    111  viewVisibleCallback() {
    112    this.start();
    113  }
    114 
    115  viewHiddenCallback() {
    116    this.stop();
    117  }
    118 
    119  static queries = {
    120    cards: { all: "card-container:not([hidden])" },
    121    migrationWizardDialog: "#migrationWizardDialog",
    122    emptyState: "fxview-empty-state",
    123    lists: { all: "fxview-tab-list" },
    124    showAllHistoryBtn: ".show-all-history-button",
    125    searchTextbox: "moz-input-search",
    126    sortInputs: { all: "input[name=history-sort-option]" },
    127    panelList: "panel-list",
    128  };
    129 
    130  static properties = {
    131    // Making profileAge a reactive property for testing
    132    profileAge: { type: Number },
    133  };
    134 
    135  async getUpdateComplete() {
    136    await super.getUpdateComplete();
    137    await Promise.all(Array.from(this.cards).map(card => card.updateComplete));
    138  }
    139 
    140  onPrimaryAction(e) {
    141    navigateToLink(e);
    142    // Record telemetry
    143    Glean.firefoxviewNext.historyVisits.record();
    144 
    145    if (this.controller.searchQuery) {
    146      Glean.firefoxview.cumulativeSearches.history.accumulateSingleSample(
    147        this.cumulativeSearches
    148      );
    149      this.cumulativeSearches = 0;
    150    }
    151  }
    152 
    153  onSecondaryAction(e) {
    154    this.triggerNode = e.originalTarget;
    155    this.panelList.toggle(e.detail.originalEvent);
    156  }
    157 
    158  deleteFromHistory(e) {
    159    this.controller.deleteFromHistory().catch(console.error);
    160    this.recordContextMenuTelemetry("delete-from-history", e);
    161  }
    162 
    163  onChangeSortOption(e) {
    164    this.controller.onChangeSortOption(e);
    165    Glean.firefoxviewNext.sortHistoryTabs.record({
    166      sort_type: this.controller.sortOption,
    167      search_start: this.controller.searchQuery ? "true" : "false",
    168    });
    169  }
    170 
    171  onSearchQuery(e) {
    172    if (!this.recentBrowsing) {
    173      Glean.firefoxviewNext.searchInitiatedSearch.record({
    174        page: "history",
    175      });
    176    }
    177    this.controller.onSearchQuery(e);
    178    this.cumulativeSearches = this.controller.searchQuery
    179      ? this.cumulativeSearches + 1
    180      : 0;
    181  }
    182 
    183  showAllHistory() {
    184    // Record telemetry
    185    Glean.firefoxviewNext.showAllHistoryTabs.record();
    186 
    187    // Open History view in Library window
    188    this.getWindow().PlacesCommandHook.showPlacesOrganizer("History");
    189  }
    190 
    191  async openMigrationWizard() {
    192    let migrationWizardDialog = this.migrationWizardDialog;
    193 
    194    if (migrationWizardDialog.open) {
    195      return;
    196    }
    197 
    198    await customElements.whenDefined("migration-wizard");
    199 
    200    // If we've been opened before, remove the old wizard and insert a
    201    // new one to put it back into its starting state.
    202    if (!migrationWizardDialog.firstElementChild) {
    203      let wizard = document.createElement("migration-wizard");
    204      wizard.toggleAttribute("dialog-mode", true);
    205      migrationWizardDialog.appendChild(wizard);
    206    }
    207    migrationWizardDialog.firstElementChild.requestState();
    208 
    209    this.migrationWizardDialog.addEventListener(
    210      "MigrationWizard:Close",
    211      function (e) {
    212        e.currentTarget.close();
    213      }
    214    );
    215 
    216    migrationWizardDialog.showModal();
    217  }
    218 
    219  shouldShowImportBanner() {
    220    return (
    221      this.profileAge < 8 &&
    222      !this.hasImportedHistoryPref &&
    223      !this.importHistoryDismissedPref &&
    224      Services.policies.isAllowed("profileImport")
    225    );
    226  }
    227 
    228  dismissImportHistory() {
    229    Services.prefs.setBoolPref(IMPORT_HISTORY_DISMISSED_PREF, true);
    230  }
    231 
    232  updated() {
    233    this.fullyUpdated = true;
    234    if (this.lists?.length) {
    235      this.toggleVisibilityInCardContainer();
    236    }
    237  }
    238 
    239  panelListTemplate() {
    240    return html`
    241      <panel-list slot="menu" data-tab-type="history">
    242        <panel-item
    243          @click=${this.deleteFromHistory}
    244          data-l10n-id="firefoxview-history-context-delete"
    245          data-l10n-attrs="accesskey"
    246        ></panel-item>
    247        <hr />
    248        <panel-item
    249          @click=${this.openInNewWindow}
    250          data-l10n-id="fxviewtabrow-open-in-window"
    251          data-l10n-attrs="accesskey"
    252        ></panel-item>
    253        <panel-item
    254          @click=${this.openInNewPrivateWindow}
    255          data-l10n-id="fxviewtabrow-open-in-private-window"
    256          data-l10n-attrs="accesskey"
    257          ?hidden=${!lazy.PrivateBrowsingUtils.enabled}
    258        ></panel-item>
    259        <hr />
    260        <panel-item
    261          @click=${this.copyLink}
    262          data-l10n-id="fxviewtabrow-copy-link"
    263          data-l10n-attrs="accesskey"
    264        ></panel-item>
    265      </panel-list>
    266    `;
    267  }
    268 
    269  /**
    270   * The template to use for cards-container.
    271   */
    272  get cardsTemplate() {
    273    if (this.controller.searchResults) {
    274      return this.#searchResultsTemplate();
    275    } else if (!this.controller.isHistoryEmpty) {
    276      return this.#historyCardsTemplate();
    277    }
    278    return this.#emptyMessageTemplate();
    279  }
    280 
    281  #historyCardsTemplate() {
    282    let cardsTemplate = [];
    283    switch (this.controller.sortOption) {
    284      case "date":
    285        cardsTemplate = this.controller.historyVisits.map(historyItem => {
    286          let dateArg = JSON.stringify({ date: historyItem.items[0].time });
    287          return html`<card-container>
    288            <h3
    289              slot="header"
    290              data-l10n-id=${historyItem.l10nId}
    291              data-l10n-args=${dateArg}
    292            ></h3>
    293            <fxview-tab-list
    294              slot="main"
    295              secondaryActionClass="options-button"
    296              dateTimeFormat=${historyItem.l10nId.includes("prev-month")
    297                ? "dateTime"
    298                : "time"}
    299              hasPopup="menu"
    300              maxTabsLength=${this.maxTabsLength}
    301              .tabItems=${historyItem.items}
    302              @fxview-tab-list-primary-action=${this.onPrimaryAction}
    303              @fxview-tab-list-secondary-action=${this.onSecondaryAction}
    304            >
    305            </fxview-tab-list>
    306          </card-container>`;
    307        });
    308        break;
    309      case "site":
    310        cardsTemplate = this.controller.historyVisits.map(historyItem => {
    311          return html`<card-container>
    312            <h3 slot="header" data-l10n-id=${ifDefined(historyItem.l10nId)}>
    313              ${historyItem.domain}
    314            </h3>
    315            <fxview-tab-list
    316              slot="main"
    317              secondaryActionClass="options-button"
    318              dateTimeFormat="dateTime"
    319              hasPopup="menu"
    320              maxTabsLength=${this.maxTabsLength}
    321              .tabItems=${historyItem.items}
    322              @fxview-tab-list-primary-action=${this.onPrimaryAction}
    323              @fxview-tab-list-secondary-action=${this.onSecondaryAction}
    324            >
    325            </fxview-tab-list>
    326          </card-container>`;
    327        });
    328        break;
    329    }
    330    return cardsTemplate;
    331  }
    332 
    333  #emptyMessageTemplate() {
    334    let descriptionHeader;
    335    let descriptionLabels;
    336    let descriptionLink;
    337    if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) {
    338      // History pref set to never remember history
    339      descriptionHeader = "firefoxview-dont-remember-history-empty-header-2";
    340      descriptionLabels = [
    341        "firefoxview-dont-remember-history-empty-description-one",
    342      ];
    343      descriptionLink = {
    344        url: "about:preferences#privacy",
    345        name: "history-settings-url-two",
    346      };
    347    } else {
    348      descriptionHeader = "firefoxview-history-empty-header";
    349      descriptionLabels = [
    350        "firefoxview-history-empty-description",
    351        "firefoxview-history-empty-description-two",
    352      ];
    353      descriptionLink = {
    354        url: "about:preferences#privacy",
    355        name: "history-settings-url",
    356      };
    357    }
    358    return html`
    359      <fxview-empty-state
    360        headerLabel=${descriptionHeader}
    361        .descriptionLabels=${descriptionLabels}
    362        .descriptionLink=${descriptionLink}
    363        class="empty-state history"
    364        ?isSelectedTab=${this.selectedTab}
    365        mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg"
    366      >
    367      </fxview-empty-state>
    368    `;
    369  }
    370 
    371  #searchResultsTemplate() {
    372    return html` <card-container toggleDisabled>
    373      <h3
    374        slot="header"
    375        data-l10n-id="firefoxview-search-results-header"
    376        data-l10n-args=${JSON.stringify({
    377          query: escapeHtmlEntities(this.controller.searchQuery),
    378        })}
    379      ></h3>
    380      ${when(
    381        this.controller.searchResults.length,
    382        () =>
    383          html`<h3
    384            slot="secondary-header"
    385            data-l10n-id="firefoxview-search-results-count"
    386            data-l10n-args=${JSON.stringify({
    387              count: this.controller.searchResults.length,
    388            })}
    389          ></h3>`
    390      )}
    391      <fxview-tab-list
    392        slot="main"
    393        secondaryActionClass="options-button"
    394        dateTimeFormat="dateTime"
    395        hasPopup="menu"
    396        maxTabsLength="-1"
    397        .searchQuery=${this.controller.searchQuery}
    398        .tabItems=${this.controller.searchResults}
    399        @fxview-tab-list-primary-action=${this.onPrimaryAction}
    400        @fxview-tab-list-secondary-action=${this.onSecondaryAction}
    401      >
    402      </fxview-tab-list>
    403    </card-container>`;
    404  }
    405 
    406  render() {
    407    if (!this.selectedTab) {
    408      return null;
    409    }
    410    return html`
    411      <link
    412        rel="stylesheet"
    413        href="chrome://browser/content/firefoxview/firefoxview.css"
    414      />
    415      <link
    416        rel="stylesheet"
    417        href="chrome://browser/content/firefoxview/history.css"
    418      />
    419      <dialog id="migrationWizardDialog"></dialog>
    420      ${this.panelListTemplate()}
    421      <div class="sticky-container bottom-fade">
    422        <h2 class="page-header" data-l10n-id="firefoxview-history-header"></h2>
    423        <div class="history-sort-options">
    424          <div class="history-sort-option">
    425            <moz-input-search
    426              data-l10n-id="firefoxview-search-text-box-history"
    427              data-l10n-attrs="placeholder"
    428              @MozInputSearch:search=${this.onSearchQuery}
    429            ></moz-input-search>
    430          </div>
    431          <div class="history-sort-option">
    432            <input
    433              type="radio"
    434              id="sort-by-date"
    435              name="history-sort-option"
    436              value="date"
    437              ?checked=${this.controller.sortOption === "date"}
    438              @click=${this.onChangeSortOption}
    439            />
    440            <label
    441              for="sort-by-date"
    442              data-l10n-id="firefoxview-sort-history-by-date-label"
    443            ></label>
    444          </div>
    445          <div class="history-sort-option">
    446            <input
    447              type="radio"
    448              id="sort-by-site"
    449              name="history-sort-option"
    450              value="site"
    451              ?checked=${this.controller.sortOption === "site"}
    452              @click=${this.onChangeSortOption}
    453            />
    454            <label
    455              for="sort-by-site"
    456              data-l10n-id="firefoxview-sort-history-by-site-label"
    457            ></label>
    458          </div>
    459        </div>
    460      </div>
    461      <div class="cards-container">
    462        <card-container
    463          class="import-history-banner"
    464          hideHeader="true"
    465          ?hidden=${!this.shouldShowImportBanner()}
    466          role="group"
    467          aria-labelledby="header"
    468          aria-describedby="description"
    469        >
    470          <div slot="main">
    471            <div class="banner-text">
    472              <span
    473                data-l10n-id="firefoxview-import-history-header"
    474                id="header"
    475              ></span>
    476              <span
    477                data-l10n-id="firefoxview-import-history-description"
    478                id="description"
    479              ></span>
    480            </div>
    481            <div class="buttons">
    482              <button
    483                class="primary choose-browser"
    484                data-l10n-id="firefoxview-choose-browser-button"
    485                @click=${this.openMigrationWizard}
    486              ></button>
    487              <moz-button
    488                class="close"
    489                type="icon ghost"
    490                data-l10n-id="firefoxview-import-history-close-button"
    491                @click=${this.dismissImportHistory}
    492              ></moz-button>
    493            </div>
    494          </div>
    495        </card-container>
    496        ${this.cardsTemplate}
    497      </div>
    498      <div
    499        class="show-all-history-footer"
    500        ?hidden=${this.controller.isHistoryEmpty}
    501      >
    502        <button
    503          class="show-all-history-button"
    504          data-l10n-id="firefoxview-show-all-history"
    505          @click=${this.showAllHistory}
    506          ?hidden=${this.controller.searchResults}
    507        ></button>
    508      </div>
    509    `;
    510  }
    511 
    512  willUpdate() {
    513    this.fullyUpdated = false;
    514  }
    515 }
    516 customElements.define("view-history", HistoryInView);