tor-browser

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

recentlyclosed.mjs (13839B)


      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  classMap,
      7  html,
      8  ifDefined,
      9  when,
     10 } from "chrome://global/content/vendor/lit.all.mjs";
     11 import { MAX_TABS_FOR_RECENT_BROWSING } from "./helpers.mjs";
     12 import { searchTabList } from "./search-helpers.mjs";
     13 import { ViewPage } from "./viewpage.mjs";
     14 // eslint-disable-next-line import/no-unassigned-import
     15 import "chrome://browser/content/firefoxview/card-container.mjs";
     16 // eslint-disable-next-line import/no-unassigned-import
     17 import "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
     18 
     19 const lazy = {};
     20 ChromeUtils.defineESModuleGetters(lazy, {
     21  SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
     22 });
     23 
     24 const SS_NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
     25 const SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH = "sessionstore-browser-shutdown-flush";
     26 const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
     27 const INCLUDE_CLOSED_TABS_FROM_CLOSED_WINDOWS =
     28  "browser.sessionstore.closedTabsFromClosedWindows";
     29 
     30 function getWindow() {
     31  return window.browsingContext.embedderWindowGlobal.browsingContext.window;
     32 }
     33 
     34 class RecentlyClosedTabsInView extends ViewPage {
     35  constructor() {
     36    super();
     37    this._started = false;
     38    this.boundObserve = (...args) => this.observe(...args);
     39    this.firstUpdateComplete = false;
     40    this.fullyUpdated = false;
     41    this.maxTabsLength = this.recentBrowsing
     42      ? MAX_TABS_FOR_RECENT_BROWSING
     43      : -1;
     44    this.recentlyClosedTabs = [];
     45    this.searchQuery = "";
     46    this.searchResults = null;
     47    this.showAll = false;
     48    this.cumulativeSearches = 0;
     49  }
     50 
     51  static properties = {
     52    ...ViewPage.properties,
     53    searchResults: { type: Array },
     54    showAll: { type: Boolean },
     55    cumulativeSearches: { type: Number },
     56  };
     57 
     58  static queries = {
     59    cardEl: "card-container",
     60    emptyState: "fxview-empty-state",
     61    searchTextbox: "moz-input-search",
     62    tabList: "fxview-tab-list",
     63  };
     64 
     65  observe(subject, topic) {
     66    if (
     67      topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED ||
     68      (topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH &&
     69        subject.ownerGlobal == getWindow())
     70    ) {
     71      this.updateRecentlyClosedTabs();
     72    }
     73  }
     74 
     75  start() {
     76    if (this._started) {
     77      return;
     78    }
     79    this._started = true;
     80    this.paused = false;
     81    this.updateRecentlyClosedTabs();
     82 
     83    Services.obs.addObserver(
     84      this.boundObserve,
     85      SS_NOTIFY_CLOSED_OBJECTS_CHANGED
     86    );
     87    Services.obs.addObserver(
     88      this.boundObserve,
     89      SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH
     90    );
     91 
     92    if (this.recentBrowsing) {
     93      this.recentBrowsingElement.addEventListener(
     94        "MozInputSearch:search",
     95        this
     96      );
     97    }
     98 
     99    this.toggleVisibilityInCardContainer();
    100  }
    101 
    102  stop() {
    103    if (!this._started) {
    104      return;
    105    }
    106    this._started = false;
    107 
    108    Services.obs.removeObserver(
    109      this.boundObserve,
    110      SS_NOTIFY_CLOSED_OBJECTS_CHANGED
    111    );
    112    Services.obs.removeObserver(
    113      this.boundObserve,
    114      SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH
    115    );
    116 
    117    if (this.recentBrowsing) {
    118      this.recentBrowsingElement.removeEventListener(
    119        "MozInputSearch:search",
    120        this
    121      );
    122    }
    123 
    124    this.toggleVisibilityInCardContainer();
    125  }
    126 
    127  disconnectedCallback() {
    128    super.disconnectedCallback();
    129    this.stop();
    130  }
    131 
    132  handleEvent(event) {
    133    if (this.recentBrowsing && event.type === "MozInputSearch:search") {
    134      this.onSearchQuery(event);
    135    }
    136  }
    137 
    138  // We remove all the observers when the instance is not visible to the user
    139  viewHiddenCallback() {
    140    this.stop();
    141  }
    142 
    143  // We add observers and check for changes to the session store once the user return to this tab.
    144  // or the instance becomes visible to the user
    145  viewVisibleCallback() {
    146    this.start();
    147  }
    148 
    149  firstUpdated() {
    150    this.firstUpdateComplete = true;
    151  }
    152 
    153  getTabStateValue(tab, key) {
    154    let value = "";
    155    const tabEntries = tab.state.entries;
    156    const activeIndex = tab.state.index - 1;
    157 
    158    if (activeIndex >= 0 && tabEntries[activeIndex]) {
    159      value = tabEntries[activeIndex][key];
    160    }
    161 
    162    return value;
    163  }
    164 
    165  updateRecentlyClosedTabs() {
    166    let recentlyClosedTabsData =
    167      lazy.SessionStore.getClosedTabData(getWindow());
    168    if (Services.prefs.getBoolPref(INCLUDE_CLOSED_TABS_FROM_CLOSED_WINDOWS)) {
    169      recentlyClosedTabsData.push(
    170        ...lazy.SessionStore.getClosedTabDataFromClosedWindows()
    171      );
    172    }
    173    // sort the aggregated list to most-recently-closed first
    174    recentlyClosedTabsData.sort((a, b) => a.closedAt < b.closedAt);
    175    this.recentlyClosedTabs = recentlyClosedTabsData;
    176    this.normalizeRecentlyClosedData();
    177    if (this.searchQuery) {
    178      this.#updateSearchResults();
    179    }
    180    this.requestUpdate();
    181  }
    182 
    183  normalizeRecentlyClosedData() {
    184    // Normalize data for fxview-tabs-list
    185    this.recentlyClosedTabs.forEach(recentlyClosedItem => {
    186      const targetURI = this.getTabStateValue(recentlyClosedItem, "url");
    187      recentlyClosedItem.time = recentlyClosedItem.closedAt;
    188      recentlyClosedItem.icon = recentlyClosedItem.image;
    189      recentlyClosedItem.primaryL10nId = "fxviewtabrow-tabs-list-tab";
    190      recentlyClosedItem.primaryL10nArgs = JSON.stringify({
    191        targetURI: typeof targetURI === "string" ? targetURI : "",
    192      });
    193      recentlyClosedItem.secondaryL10nId =
    194        "firefoxview-closed-tabs-dismiss-tab";
    195      recentlyClosedItem.secondaryL10nArgs = JSON.stringify({
    196        tabTitle: recentlyClosedItem.title,
    197      });
    198      recentlyClosedItem.url = targetURI;
    199    });
    200  }
    201 
    202  onReopenTab(e) {
    203    const closedId = parseInt(e.originalTarget.closedId, 10);
    204    const sourceClosedId = parseInt(e.originalTarget.sourceClosedId, 10);
    205    if (isNaN(sourceClosedId)) {
    206      lazy.SessionStore.undoCloseById(closedId, getWindow());
    207    } else {
    208      lazy.SessionStore.undoClosedTabFromClosedWindow(
    209        { sourceClosedId },
    210        closedId,
    211        getWindow()
    212      );
    213    }
    214 
    215    // Record telemetry
    216    let tabClosedAt = parseInt(e.originalTarget.time);
    217    const position =
    218      Array.from(this.tabList.rowEls).indexOf(e.originalTarget) + 1;
    219 
    220    let now = Date.now();
    221    let deltaSeconds = (now - tabClosedAt) / 1000;
    222    Glean.firefoxviewNext.recentlyClosedTabs.record({
    223      position,
    224      delta: deltaSeconds,
    225      page: this.recentBrowsing ? "recentbrowsing" : "recentlyclosed",
    226    });
    227    if (this.searchQuery) {
    228      Glean.firefoxview.cumulativeSearches[
    229        this.recentBrowsing ? "recentbrowsing" : "recentlyclosed"
    230      ].accumulateSingleSample(this.cumulativeSearches);
    231      this.cumulativeSearches = 0;
    232    }
    233  }
    234 
    235  onDismissTab(e) {
    236    const closedId = parseInt(e.originalTarget.closedId, 10);
    237    const sourceClosedId = parseInt(e.originalTarget.sourceClosedId, 10);
    238    const sourceWindowId = e.originalTarget.sourceWindowId;
    239    if (!isNaN(sourceClosedId)) {
    240      // the sourceClosedId is an identifier for a now-closed window the tab
    241      // was closed in.
    242      lazy.SessionStore.forgetClosedTabById(closedId, {
    243        sourceClosedId,
    244      });
    245    } else if (sourceWindowId) {
    246      // the sourceWindowId is an identifier for a currently-open window the tab
    247      // was closed in.
    248      lazy.SessionStore.forgetClosedTabById(closedId, {
    249        sourceWindowId,
    250      });
    251    } else {
    252      // without either identifier, SessionStore will need to walk its window collections
    253      // to find the close tab with matching closedId
    254      lazy.SessionStore.forgetClosedTabById(closedId);
    255    }
    256 
    257    // Record telemetry
    258    let tabClosedAt = parseInt(e.originalTarget.time);
    259    const position =
    260      Array.from(this.tabList.rowEls).indexOf(e.originalTarget) + 1;
    261 
    262    let now = Date.now();
    263    let deltaSeconds = (now - tabClosedAt) / 1000;
    264    Glean.firefoxviewNext.dismissClosedTabTabs.record({
    265      position,
    266      delta: deltaSeconds,
    267      page: this.recentBrowsing ? "recentbrowsing" : "recentlyclosed",
    268    });
    269  }
    270 
    271  willUpdate() {
    272    this.fullyUpdated = false;
    273  }
    274 
    275  updated() {
    276    this.fullyUpdated = true;
    277    this.toggleVisibilityInCardContainer();
    278  }
    279 
    280  async scheduleUpdate() {
    281    // Only defer initial update
    282    if (!this.firstUpdateComplete) {
    283      await new Promise(resolve => setTimeout(resolve));
    284    }
    285    super.scheduleUpdate();
    286  }
    287 
    288  emptyMessageTemplate() {
    289    let descriptionHeader;
    290    let descriptionLabels;
    291    let descriptionLink;
    292    if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) {
    293      // History pref set to never remember history
    294      descriptionHeader = "firefoxview-dont-remember-history-empty-header-2";
    295      descriptionLabels = [
    296        "firefoxview-dont-remember-history-empty-description-one",
    297      ];
    298      descriptionLink = {
    299        url: "about:preferences#privacy",
    300        name: "history-settings-url-two",
    301      };
    302    } else {
    303      descriptionHeader = "firefoxview-recentlyclosed-empty-header";
    304      descriptionLabels = [
    305        "firefoxview-recentlyclosed-empty-description",
    306        "firefoxview-recentlyclosed-empty-description-two",
    307      ];
    308      descriptionLink = {
    309        url: "about:firefoxview#history",
    310        name: "history-url",
    311        sameTarget: "true",
    312      };
    313    }
    314    return html`
    315      <fxview-empty-state
    316        headerLabel=${descriptionHeader}
    317        .descriptionLabels=${descriptionLabels}
    318        .descriptionLink=${descriptionLink}
    319        class="empty-state recentlyclosed"
    320        ?isInnerCard=${this.recentBrowsing}
    321        ?isSelectedTab=${this.selectedTab}
    322        mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg"
    323      >
    324      </fxview-empty-state>
    325    `;
    326  }
    327 
    328  render() {
    329    return html`
    330      <link
    331        rel="stylesheet"
    332        href="chrome://browser/content/firefoxview/firefoxview.css"
    333      />
    334      ${when(
    335        !this.recentBrowsing,
    336        () =>
    337          html`<div
    338            class="sticky-container bottom-fade"
    339            ?hidden=${!this.selectedTab}
    340          >
    341            <h2
    342              class="page-header"
    343              data-l10n-id="firefoxview-recently-closed-header"
    344            ></h2>
    345            <div>
    346              <moz-input-search
    347                data-l10n-id="firefoxview-search-text-box-recentlyclosed"
    348                data-l10n-attrs="placeholder"
    349                @MozInputSearch:search=${this.onSearchQuery}
    350              ></moz-input-search>
    351            </div>
    352          </div>`
    353      )}
    354      <div class=${classMap({ "cards-container": this.selectedTab })}>
    355        <card-container
    356          shortPageName=${this.recentBrowsing ? "recentlyclosed" : null}
    357          ?showViewAll=${this.recentBrowsing && this.recentlyClosedTabs.length}
    358          ?preserveCollapseState=${this.recentBrowsing ? true : null}
    359          ?hideHeader=${this.selectedTab}
    360          ?hidden=${!this.recentlyClosedTabs.length && !this.recentBrowsing}
    361          ?isEmptyState=${!this.recentlyClosedTabs.length}
    362        >
    363          <h3
    364            slot="header"
    365            data-l10n-id="firefoxview-recently-closed-header"
    366          ></h3>
    367          ${when(
    368            this.recentlyClosedTabs.length,
    369            () => html`
    370              <fxview-tab-list
    371                slot="main"
    372                .maxTabsLength=${!this.recentBrowsing || this.showAll
    373                  ? -1
    374                  : MAX_TABS_FOR_RECENT_BROWSING}
    375                .searchQuery=${ifDefined(
    376                  this.searchResults && this.searchQuery
    377                )}
    378                .tabItems=${this.searchResults || this.recentlyClosedTabs}
    379                @fxview-tab-list-secondary-action=${this.onDismissTab}
    380                @fxview-tab-list-primary-action=${this.onReopenTab}
    381                secondaryActionClass="dismiss-button"
    382              ></fxview-tab-list>
    383            `
    384          )}
    385          ${when(
    386            this.recentBrowsing && !this.recentlyClosedTabs.length,
    387            () => html` <div slot="main">${this.emptyMessageTemplate()}</div> `
    388          )}
    389          ${when(
    390            this.isShowAllLinkVisible(),
    391            () =>
    392              html` <div
    393                @click=${this.enableShowAll}
    394                @keydown=${this.enableShowAll}
    395                data-l10n-id="firefoxview-show-all"
    396                ?hidden=${!this.isShowAllLinkVisible()}
    397                slot="footer"
    398                tabindex="0"
    399                role="link"
    400              ></div>`
    401          )}
    402        </card-container>
    403        ${when(
    404          this.selectedTab && !this.recentlyClosedTabs.length,
    405          () => html` <div>${this.emptyMessageTemplate()}</div> `
    406        )}
    407      </div>
    408    `;
    409  }
    410 
    411  onSearchQuery(e) {
    412    if (!this.recentBrowsing) {
    413      Glean.firefoxviewNext.searchInitiatedSearch.record({
    414        page: "recentlyclosed",
    415      });
    416    }
    417    this.searchQuery = e.detail.query;
    418    this.showAll = false;
    419    this.cumulativeSearches = this.searchQuery
    420      ? this.cumulativeSearches + 1
    421      : 0;
    422    this.#updateSearchResults();
    423  }
    424 
    425  #updateSearchResults() {
    426    this.searchResults = this.searchQuery
    427      ? searchTabList(this.searchQuery, this.recentlyClosedTabs)
    428      : null;
    429  }
    430 
    431  isShowAllLinkVisible() {
    432    return (
    433      this.recentBrowsing &&
    434      this.searchQuery &&
    435      this.searchResults.length > MAX_TABS_FOR_RECENT_BROWSING &&
    436      !this.showAll
    437    );
    438  }
    439 
    440  enableShowAll(event) {
    441    if (
    442      event.type == "click" ||
    443      (event.type == "keydown" && event.code == "Enter") ||
    444      (event.type == "keydown" && event.code == "Space")
    445    ) {
    446      event.preventDefault();
    447      this.showAll = true;
    448      Glean.firefoxviewNext.searchShowAllShowallbutton.record({
    449        section: "recentlyclosed",
    450      });
    451    }
    452  }
    453 }
    454 customElements.define("view-recentlyclosed", RecentlyClosedTabsInView);