tor-browser

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

syncedtabs.mjs (13398B)


      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 const lazy = {};
      6 ChromeUtils.defineESModuleGetters(lazy, {
      7  SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs",
      8 });
      9 
     10 const { TabsSetupFlowManager } = ChromeUtils.importESModule(
     11  "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
     12 );
     13 
     14 import {
     15  html,
     16  ifDefined,
     17  when,
     18 } from "chrome://global/content/vendor/lit.all.mjs";
     19 import { ViewPage } from "./viewpage.mjs";
     20 import {
     21  escapeHtmlEntities,
     22  MAX_TABS_FOR_RECENT_BROWSING,
     23  navigateToLink,
     24 } from "./helpers.mjs";
     25 // eslint-disable-next-line import/no-unassigned-import
     26 import "chrome://browser/content/firefoxview/syncedtabs-tab-list.mjs";
     27 
     28 const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open";
     29 
     30 class SyncedTabsInView extends ViewPage {
     31  controller = new lazy.SyncedTabsController(this, {
     32    contextMenu: true,
     33    pairDeviceCallback: () =>
     34      Glean.firefoxviewNext.fxaMobileSync.record({
     35        has_devices: TabsSetupFlowManager.secondaryDeviceConnected,
     36      }),
     37    signupCallback: () => Glean.firefoxviewNext.fxaContinueSync.record(),
     38  });
     39 
     40  constructor() {
     41    super();
     42    this._started = false;
     43    this._id = Math.floor(Math.random() * 10e6);
     44    if (this.recentBrowsing) {
     45      this.maxTabsLength = MAX_TABS_FOR_RECENT_BROWSING;
     46    } else {
     47      // Setting maxTabsLength to -1 for no max
     48      this.maxTabsLength = -1;
     49    }
     50    this.fullyUpdated = false;
     51    this.showAll = false;
     52    this.cumulativeSearches = 0;
     53    this.onSearchQuery = this.onSearchQuery.bind(this);
     54  }
     55 
     56  static properties = {
     57    ...ViewPage.properties,
     58    showAll: { type: Boolean },
     59    cumulativeSearches: { type: Number },
     60  };
     61 
     62  static queries = {
     63    cardEls: { all: "card-container" },
     64    emptyState: "fxview-empty-state",
     65    searchTextbox: "moz-input-search",
     66    tabLists: { all: "syncedtabs-tab-list" },
     67  };
     68 
     69  start() {
     70    if (this._started) {
     71      return;
     72    }
     73    this._started = true;
     74    this.controller.addSyncObservers();
     75    this.controller.updateStates();
     76    this.onVisibilityChange();
     77 
     78    if (this.recentBrowsing) {
     79      this.recentBrowsingElement.addEventListener(
     80        "MozInputSearch:search",
     81        this.onSearchQuery
     82      );
     83    }
     84  }
     85 
     86  stop() {
     87    if (!this._started) {
     88      return;
     89    }
     90    this._started = false;
     91    TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded");
     92    this.onVisibilityChange();
     93    this.controller.removeSyncObservers();
     94 
     95    if (this.recentBrowsing) {
     96      this.recentBrowsingElement.removeEventListener(
     97        "MozInputSearch:search",
     98        this.onSearchQuery
     99      );
    100    }
    101  }
    102 
    103  disconnectedCallback() {
    104    super.disconnectedCallback();
    105    this.stop();
    106  }
    107 
    108  viewVisibleCallback() {
    109    this.start();
    110  }
    111 
    112  viewHiddenCallback() {
    113    this.stop();
    114  }
    115 
    116  onVisibilityChange() {
    117    const isOpen = this.open;
    118    const isVisible = this.isVisible;
    119    if (isVisible && isOpen) {
    120      this.update();
    121      TabsSetupFlowManager.updateViewVisibility(this._id, "visible");
    122    } else {
    123      TabsSetupFlowManager.updateViewVisibility(
    124        this._id,
    125        isVisible ? "closed" : "hidden"
    126      );
    127    }
    128 
    129    this.toggleVisibilityInCardContainer();
    130  }
    131 
    132  generateMessageCard({
    133    action,
    134    buttonLabel,
    135    descriptionArray,
    136    descriptionLink,
    137    header,
    138    mainImageUrl,
    139  }) {
    140    return html`
    141      <fxview-empty-state
    142        headerLabel=${header}
    143        .descriptionLabels=${descriptionArray}
    144        .descriptionLink=${ifDefined(descriptionLink)}
    145        class="empty-state synced-tabs error"
    146        ?isSelectedTab=${this.selectedTab}
    147        ?isInnerCard=${this.recentBrowsing}
    148        mainImageUrl=${ifDefined(mainImageUrl)}
    149        id="empty-container"
    150      >
    151        <button
    152          class="primary"
    153          slot="primary-action"
    154          ?hidden=${!buttonLabel}
    155          data-l10n-id=${ifDefined(buttonLabel)}
    156          data-action=${action}
    157          @click=${e => this.controller.handleEvent(e)}
    158        ></button>
    159      </fxview-empty-state>
    160    `;
    161  }
    162 
    163  onOpenLink(event) {
    164    navigateToLink(event);
    165 
    166    Glean.firefoxviewNext.syncedTabsTabs.record({
    167      page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs",
    168    });
    169 
    170    if (this.controller.searchQuery) {
    171      Glean.firefoxview.cumulativeSearches[
    172        this.recentBrowsing ? "recentbrowsing" : "syncedtabs"
    173      ].accumulateSingleSample(this.cumulativeSearches);
    174      this.cumulativeSearches = 0;
    175    }
    176  }
    177 
    178  onContextMenu(e) {
    179    this.triggerNode = e.originalTarget;
    180    e.target.querySelector("panel-list").toggle(e.detail.originalEvent);
    181  }
    182 
    183  onCloseTab(e) {
    184    const { url, fxaDeviceId, tertiaryActionClass } = e.originalTarget;
    185    if (tertiaryActionClass === "dismiss-button") {
    186      // Set new pending close tab
    187      this.controller.requestCloseRemoteTab(fxaDeviceId, url);
    188    } else if (tertiaryActionClass === "undo-button") {
    189      // User wants to undo
    190      this.controller.removePendingTabToClose(fxaDeviceId, url);
    191    }
    192    this.requestUpdate();
    193  }
    194 
    195  panelListTemplate() {
    196    return html`
    197      <panel-list slot="menu" data-tab-type="syncedtabs">
    198        <panel-item
    199          @click=${this.openInNewWindow}
    200          data-l10n-id="fxviewtabrow-open-in-window"
    201          data-l10n-attrs="accesskey"
    202        ></panel-item>
    203        <panel-item
    204          @click=${this.openInNewPrivateWindow}
    205          data-l10n-id="fxviewtabrow-open-in-private-window"
    206          data-l10n-attrs="accesskey"
    207        ></panel-item>
    208        <hr />
    209        <panel-item
    210          @click=${this.copyLink}
    211          data-l10n-id="fxviewtabrow-copy-link"
    212          data-l10n-attrs="accesskey"
    213        ></panel-item>
    214      </panel-list>
    215    `;
    216  }
    217 
    218  noDeviceTabsTemplate(deviceName, deviceType, isSearchResultsEmpty = false) {
    219    const template = html`<h3
    220        slot=${ifDefined(this.recentBrowsing ? null : "header")}
    221        class="device-header"
    222      >
    223        <span class="icon ${deviceType}" role="presentation"></span>
    224        ${deviceName}
    225      </h3>
    226      ${when(
    227        isSearchResultsEmpty,
    228        () => html`
    229          <div
    230            slot=${ifDefined(this.recentBrowsing ? null : "main")}
    231            class="blackbox notabs search-results-empty"
    232            data-l10n-id="firefoxview-search-results-empty"
    233            data-l10n-args=${JSON.stringify({
    234              query: escapeHtmlEntities(this.controller.searchQuery),
    235            })}
    236          ></div>
    237        `,
    238        () => html`
    239          <div
    240            slot=${ifDefined(this.recentBrowsing ? null : "main")}
    241            class="blackbox notabs"
    242            data-l10n-id="firefoxview-syncedtabs-device-notabs"
    243          ></div>
    244        `
    245      )}`;
    246    return this.recentBrowsing
    247      ? template
    248      : html`<card-container
    249          shortPageName=${this.recentBrowsing ? "syncedtabs" : null}
    250          >${template}</card-container
    251        >`;
    252  }
    253 
    254  onSearchQuery(e) {
    255    if (!this.recentBrowsing) {
    256      Glean.firefoxviewNext.searchInitiatedSearch.record({
    257        page: "syncedtabs",
    258      });
    259    }
    260    this.controller.searchQuery = e.detail.query;
    261    this.cumulativeSearches = e.detail.query ? this.cumulativeSearches + 1 : 0;
    262    this.showAll = false;
    263  }
    264 
    265  deviceTemplate(deviceName, deviceType, tabItems) {
    266    return html`<h3
    267        slot=${!this.recentBrowsing ? "header" : null}
    268        class="device-header"
    269      >
    270        <span class="icon ${deviceType}" role="presentation"></span>
    271        ${deviceName}
    272      </h3>
    273      <syncedtabs-tab-list
    274        slot="main"
    275        .hasPopup=${"menu"}
    276        .tabItems=${ifDefined(tabItems)}
    277        .searchQuery=${this.controller.searchQuery}
    278        .maxTabsLength=${this.showAll ? -1 : this.maxTabsLength}
    279        @fxview-tab-list-primary-action=${this.onOpenLink}
    280        @fxview-tab-list-secondary-action=${this.onContextMenu}
    281        @fxview-tab-list-tertiary-action=${this.onCloseTab}
    282        secondaryActionClass="options-button"
    283      >
    284        ${this.panelListTemplate()}
    285      </syncedtabs-tab-list>`;
    286  }
    287 
    288  generateTabList() {
    289    let renderArray = [];
    290    let renderInfo = this.controller.getRenderInfo();
    291    for (let id in renderInfo) {
    292      let tabItems = renderInfo[id].tabItems;
    293      if (tabItems.length) {
    294        const template = this.recentBrowsing
    295          ? this.deviceTemplate(
    296              renderInfo[id].name,
    297              renderInfo[id].deviceType,
    298              tabItems
    299            )
    300          : html`<card-container
    301              shortPageName=${this.recentBrowsing ? "syncedtabs" : null}
    302              >${this.deviceTemplate(
    303                renderInfo[id].name,
    304                renderInfo[id].deviceType,
    305                tabItems
    306              )}
    307            </card-container>`;
    308        renderArray.push(template);
    309        if (this.isShowAllLinkVisible(tabItems)) {
    310          renderArray.push(
    311            html` <div class="show-all-link-container">
    312              <div
    313                class="show-all-link"
    314                @click=${this.enableShowAll}
    315                @keydown=${this.enableShowAll}
    316                data-l10n-id="firefoxview-show-all"
    317                tabindex="0"
    318                role="link"
    319              ></div>
    320            </div>`
    321          );
    322        }
    323      } else {
    324        // Check renderInfo[id].tabs.length to determine whether to display an
    325        // empty tab list message or empty search results message.
    326        // If there are no synced tabs, we always display the empty tab list
    327        // message, even if there is an active search query.
    328        renderArray.push(
    329          this.noDeviceTabsTemplate(
    330            renderInfo[id].name,
    331            renderInfo[id].deviceType,
    332            Boolean(renderInfo[id].tabs.length)
    333          )
    334        );
    335      }
    336    }
    337    return renderArray;
    338  }
    339 
    340  isShowAllLinkVisible(tabItems) {
    341    return (
    342      this.recentBrowsing &&
    343      this.controller.searchQuery &&
    344      tabItems.length > this.maxTabsLength &&
    345      !this.showAll
    346    );
    347  }
    348 
    349  enableShowAll(event) {
    350    if (
    351      event.type == "click" ||
    352      (event.type == "keydown" && event.code == "Enter") ||
    353      (event.type == "keydown" && event.code == "Space")
    354    ) {
    355      event.preventDefault();
    356      this.showAll = true;
    357      Glean.firefoxviewNext.searchShowAllShowallbutton.record({
    358        section: "syncedtabs",
    359      });
    360    }
    361  }
    362 
    363  generateCardContent() {
    364    const cardProperties = this.controller.getMessageCard();
    365    return cardProperties
    366      ? this.generateMessageCard(cardProperties)
    367      : this.generateTabList();
    368  }
    369 
    370  render() {
    371    this.open =
    372      !TabsSetupFlowManager.isTabSyncSetupComplete ||
    373      Services.prefs.getBoolPref(UI_OPEN_STATE, true);
    374 
    375    let renderArray = [];
    376    renderArray.push(
    377      html` <link
    378        rel="stylesheet"
    379        href="chrome://browser/content/firefoxview/view-syncedtabs.css"
    380      />`
    381    );
    382    renderArray.push(
    383      html` <link
    384        rel="stylesheet"
    385        href="chrome://browser/content/firefoxview/firefoxview.css"
    386      />`
    387    );
    388 
    389    if (!this.recentBrowsing) {
    390      renderArray.push(
    391        html`<div class="sticky-container bottom-fade">
    392          <h2
    393            class="page-header"
    394            data-l10n-id="firefoxview-synced-tabs-header"
    395          ></h2>
    396          <div class="syncedtabs-header">
    397            <div>
    398              <moz-input-search
    399                data-l10n-id="firefoxview-search-text-box-tabs"
    400                data-l10n-attrs="placeholder"
    401                @MozInputSearch:search=${this.onSearchQuery}
    402              ></moz-input-search>
    403            </div>
    404            ${when(
    405              this.controller.currentSetupStateIndex === 4,
    406              () => html`
    407                <button
    408                  class="small-button"
    409                  data-action="add-device"
    410                  @click=${e => this.controller.handleEvent(e)}
    411                >
    412                  <img
    413                    class="icon"
    414                    role="presentation"
    415                    src="chrome://global/skin/icons/plus.svg"
    416                    alt="plus sign"
    417                  /><span
    418                    data-l10n-id="firefoxview-syncedtabs-connect-another-device"
    419                    data-action="add-device"
    420                  ></span>
    421                </button>
    422              `
    423            )}
    424          </div>
    425        </div>`
    426      );
    427    }
    428 
    429    if (this.recentBrowsing) {
    430      renderArray.push(
    431        html`<card-container
    432          preserveCollapseState
    433          shortPageName="syncedtabs"
    434          ?showViewAll=${this.controller.currentSetupStateIndex == 4 &&
    435          this.controller.currentSyncedTabs.length}
    436          ?isEmptyState=${!this.controller.currentSyncedTabs.length}
    437        >
    438          >
    439          <h3
    440            slot="header"
    441            data-l10n-id="firefoxview-synced-tabs-header"
    442            class="recentbrowsing-header"
    443          ></h3>
    444          <div slot="main">${this.generateCardContent()}</div>
    445        </card-container>`
    446      );
    447    } else {
    448      renderArray.push(
    449        html`<div class="cards-container">${this.generateCardContent()}</div>`
    450      );
    451    }
    452    return renderArray;
    453  }
    454 
    455  updated() {
    456    this.fullyUpdated = true;
    457    this.toggleVisibilityInCardContainer();
    458  }
    459 }
    460 customElements.define("view-syncedtabs", SyncedTabsInView);