tor-browser

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

sidebar-syncedtabs.mjs (10264B)


      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  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
      8  SyncedTabsController: "resource:///modules/SyncedTabsController.sys.mjs",
      9  SidebarTreeView:
     10    "moz-src:///browser/components/sidebar/SidebarTreeView.sys.mjs",
     11 });
     12 
     13 import {
     14  html,
     15  ifDefined,
     16  when,
     17 } from "chrome://global/content/vendor/lit.all.mjs";
     18 import {
     19  escapeHtmlEntities,
     20  navigateToLink,
     21 } from "chrome://browser/content/firefoxview/helpers.mjs";
     22 
     23 import { SidebarPage } from "./sidebar-page.mjs";
     24 
     25 class SyncedTabsInSidebar extends SidebarPage {
     26  controller = new lazy.SyncedTabsController(this);
     27 
     28  static queries = {
     29    cards: { all: "moz-card" },
     30    lists: { all: "sidebar-tab-list" },
     31    searchTextbox: "moz-input-search",
     32  };
     33 
     34  constructor() {
     35    super();
     36    this.onSearchQuery = this.onSearchQuery.bind(this);
     37    this.onSecondaryAction = this.onSecondaryAction.bind(this);
     38    this.treeView = new lazy.SidebarTreeView(this, { multiSelect: false });
     39  }
     40 
     41  connectedCallback() {
     42    super.connectedCallback();
     43    this.controller.addSyncObservers();
     44    this.controller.updateStates().then(() =>
     45      Glean.syncedTabs.sidebarToggle.record({
     46        opened: true,
     47        synced_tabs_loaded: this.controller.isSyncedTabsLoaded,
     48        version: "new",
     49      })
     50    );
     51    this.addContextMenuListeners();
     52    this.addSidebarFocusedListeners();
     53  }
     54 
     55  disconnectedCallback() {
     56    super.disconnectedCallback();
     57    this.controller.removeSyncObservers();
     58    Glean.syncedTabs.sidebarToggle.record({
     59      opened: false,
     60      synced_tabs_loaded: this.controller.isSyncedTabsLoaded,
     61      version: "new",
     62    });
     63    this.removeContextMenuListeners();
     64    this.removeSidebarFocusedListeners();
     65  }
     66 
     67  handleContextMenuEvent(e) {
     68    this.triggerNode =
     69      this.findTriggerNode(e, "sidebar-tab-row") ||
     70      this.findTriggerNode(e, "moz-input-search");
     71    if (!this.triggerNode) {
     72      e.preventDefault();
     73      return;
     74    }
     75    const contextMenu = this._contextMenu;
     76    const closeTabMenuItem = contextMenu.querySelector(
     77      "#sidebar-context-menu-close-remote-tab"
     78    );
     79    closeTabMenuItem.setAttribute(
     80      "data-l10n-args",
     81      this.triggerNode.secondaryL10nArgs
     82    );
     83    // Enable the feature only if the device supports it
     84    closeTabMenuItem.disabled = !this.triggerNode.canClose;
     85 
     86    let privateWindowMenuItem = contextMenu.querySelector(
     87      "#sidebar-synced-tabs-context-open-in-private-window"
     88    );
     89    privateWindowMenuItem.hidden = !lazy.PrivateBrowsingUtils.enabled;
     90  }
     91 
     92  handleCommandEvent(e) {
     93    switch (e.target.id) {
     94      case "sidebar-context-menu-close-remote-tab":
     95        this.requestOrRemoveTabToClose(
     96          this.triggerNode.url,
     97          this.triggerNode.fxaDeviceId,
     98          this.triggerNode.secondaryActionClass
     99        );
    100        break;
    101      default:
    102        super.handleCommandEvent(e);
    103        break;
    104    }
    105  }
    106 
    107  handleSidebarFocusedEvent() {
    108    this.searchTextbox?.focus();
    109  }
    110 
    111  onSecondaryAction(e) {
    112    const { url, fxaDeviceId, secondaryActionClass } = e.originalTarget;
    113    this.requestOrRemoveTabToClose(url, fxaDeviceId, secondaryActionClass);
    114  }
    115 
    116  requestOrRemoveTabToClose(url, fxaDeviceId, secondaryActionClass) {
    117    if (secondaryActionClass === "dismiss-button") {
    118      // Set new pending close tab
    119      this.controller.requestCloseRemoteTab(fxaDeviceId, url);
    120    } else if (secondaryActionClass === "undo-button") {
    121      // User wants to undo
    122      this.controller.removePendingTabToClose(fxaDeviceId, url);
    123    }
    124    this.requestUpdate();
    125  }
    126 
    127  /**
    128   * The template shown when the list of synced devices is currently
    129   * unavailable.
    130   *
    131   * @param {object} options
    132   * @param {string} options.action
    133   * @param {string} options.buttonLabel
    134   * @param {string[]} options.descriptionArray
    135   * @param {string} options.descriptionLink
    136   * @param {string} options.header
    137   * @param {string} options.mainImageUrl
    138   * @returns {TemplateResult}
    139   */
    140  messageCardTemplate({
    141    action,
    142    buttonLabel,
    143    descriptionArray,
    144    descriptionLink,
    145    header,
    146    mainImageUrl,
    147  }) {
    148    return html`
    149      <fxview-empty-state
    150        headerLabel=${header}
    151        .descriptionLabels=${descriptionArray}
    152        .descriptionLink=${ifDefined(descriptionLink)}
    153        class="empty-state synced-tabs error"
    154        isSelectedTab
    155        mainImageUrl=${ifDefined(mainImageUrl)}
    156        id="empty-container"
    157      >
    158        <moz-button
    159          type="primary"
    160          slot="primary-action"
    161          ?hidden=${!buttonLabel}
    162          data-l10n-id=${ifDefined(buttonLabel)}
    163          data-action=${action}
    164          @click=${e => this.controller.handleEvent(e)}
    165        ></moz-button>
    166      </fxview-empty-state>
    167    `;
    168  }
    169 
    170  /**
    171   * The template shown for a device that has tabs.
    172   *
    173   * @param {string} deviceName
    174   * @param {string} deviceType
    175   * @param {Array} tabItems
    176   * @returns {TemplateResult}
    177   */
    178  deviceTemplate(deviceName, deviceType, tabItems) {
    179    return html`<moz-card
    180      type="accordion"
    181      expanded
    182      .heading=${deviceName}
    183      .iconSrc=${this.getDeviceIconSrc(deviceType)}
    184      class=${deviceType}
    185      @keydown=${e => this.treeView.handleCardKeydown(e)}
    186    >
    187      <sidebar-tab-list
    188        compactRows
    189        maxTabsLength="-1"
    190        .tabItems=${tabItems}
    191        .multiSelect=${false}
    192        .updatesPaused=${false}
    193        .searchQuery=${this.controller.searchQuery}
    194        @fxview-tab-list-primary-action=${navigateToLink}
    195        @fxview-tab-list-secondary-action=${this.onSecondaryAction}
    196      ></sidebar-tab-list>
    197    </moz-card>`;
    198  }
    199 
    200  /**
    201   * The template shown for a device that has no tabs.
    202   *
    203   * @param {string} deviceName
    204   * @param {string} deviceType
    205   * @returns {TemplateResult}
    206   */
    207  noDeviceTabsTemplate(deviceName, deviceType) {
    208    return html`<moz-card
    209      .heading=${deviceName}
    210      .iconSrc=${this.getDeviceIconSrc(deviceType)}
    211      class=${deviceType}
    212      data-l10n-id="firefoxview-syncedtabs-device-notabs"
    213    >
    214    </moz-card>`;
    215  }
    216 
    217  /**
    218   * The template shown for a device that has tabs, but no tabs that match the
    219   * current search query.
    220   *
    221   * @param {string} deviceName
    222   * @param {string} deviceType
    223   * @returns {TemplateResult}
    224   */
    225  noSearchResultsTemplate(deviceName, deviceType) {
    226    return html`<moz-card
    227      .heading=${deviceName}
    228      .iconSrc=${this.getDeviceIconSrc(deviceType)}
    229      class=${deviceType}
    230      data-l10n-id="firefoxview-search-results-empty"
    231      data-l10n-args=${JSON.stringify({
    232        query: escapeHtmlEntities(this.controller.searchQuery),
    233      })}
    234    >
    235    </moz-card>`;
    236  }
    237 
    238  /**
    239   * The template shown for the list of synced devices.
    240   *
    241   * @returns {TemplateResult[]}
    242   */
    243  deviceListTemplate() {
    244    return Object.values(this.controller.getRenderInfo()).map(
    245      ({ name: deviceName, deviceType, tabItems, canClose, tabs }) => {
    246        if (tabItems.length) {
    247          return this.deviceTemplate(
    248            deviceName,
    249            deviceType,
    250            this.getTabItems(tabItems, deviceName, canClose)
    251          );
    252        } else if (tabs.length) {
    253          return this.noSearchResultsTemplate(deviceName, deviceType);
    254        }
    255        return this.noDeviceTabsTemplate(deviceName, deviceType);
    256      }
    257    );
    258  }
    259 
    260  getTabItems(items, deviceName, canClose) {
    261    return items
    262      .map(item => {
    263        // We always show the option to close remotely on right-click but
    264        // disable it if the device doesn't support actually closing it
    265        let secondaryL10nId = "synced-tabs-context-close-tab-title";
    266        let secondaryL10nArgs = JSON.stringify({ deviceName });
    267        if (!canClose) {
    268          return {
    269            ...item,
    270            canClose,
    271            secondaryL10nId,
    272            secondaryL10nArgs,
    273          };
    274        }
    275 
    276        // Default show the close/dismiss button
    277        let secondaryActionClass = "dismiss-button";
    278        item.closeRequested = false;
    279 
    280        // If this item has been requested to be closed, show
    281        // the undo instead
    282        if (item.url === this.controller.lastClosedURL) {
    283          secondaryActionClass = "undo-button";
    284          secondaryL10nId = "text-action-undo";
    285          secondaryL10nArgs = null;
    286          item.closeRequested = true;
    287        }
    288 
    289        return {
    290          ...item,
    291          canClose,
    292          secondaryActionClass,
    293          secondaryL10nId,
    294          secondaryL10nArgs,
    295        };
    296      })
    297      .filter(
    298        item =>
    299          !this.controller.isURLQueuedToClose(item.fxaDeviceId, item.url) ||
    300          item.url === this.controller.lastClosedURL
    301      );
    302  }
    303 
    304  getDeviceIconSrc(deviceType) {
    305    const phone = "chrome://browser/skin/device-phone.svg";
    306    const desktop = "chrome://browser/skin/device-desktop.svg";
    307    const tablet = "chrome://browser/skin/device-tablet.svg";
    308 
    309    const deviceIcons = {
    310      desktop,
    311      mobile: phone,
    312      phone,
    313      tablet,
    314    };
    315 
    316    return deviceIcons[deviceType] || null;
    317  }
    318 
    319  render() {
    320    const messageCard = this.controller.getMessageCard();
    321    return html`
    322      ${this.stylesheet()}
    323      <div class="sidebar-panel">
    324        <sidebar-panel-header
    325          data-l10n-id="sidebar-menu-syncedtabs-header"
    326          data-l10n-attrs="heading"
    327          view="viewTabsSidebar"
    328        >
    329          <moz-input-search
    330            data-l10n-id="firefoxview-search-text-box-tabs"
    331            data-l10n-attrs="placeholder"
    332            @MozInputSearch:search=${this.onSearchQuery}
    333          ></moz-input-search>
    334        </sidebar-panel-header>
    335        <div class="sidebar-panel-scrollable-content">
    336          ${when(
    337            messageCard,
    338            () => this.messageCardTemplate(messageCard),
    339            () => html`${this.deviceListTemplate()}`
    340          )}
    341        </div>
    342      </div>
    343    `;
    344  }
    345 
    346  onSearchQuery(e) {
    347    this.controller.searchQuery = e.detail.query;
    348    this.requestUpdate();
    349  }
    350 }
    351 
    352 customElements.define("sidebar-syncedtabs", SyncedTabsInSidebar);