mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	Remove transitive dependency on `ChromeUtils` that is causing this error. Differential Revision: https://phabricator.services.mozilla.com/D208380
		
			
				
	
	
		
			333 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			333 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 | 
						|
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
 | 
						|
  SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
import { SyncedTabsErrorHandler } from "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs";
 | 
						|
import { TabsSetupFlowManager } from "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs";
 | 
						|
import { searchTabList } from "chrome://browser/content/firefoxview/search-helpers.mjs";
 | 
						|
 | 
						|
const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
 | 
						|
const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
 | 
						|
 | 
						|
/**
 | 
						|
 * The controller for synced tabs components.
 | 
						|
 *
 | 
						|
 * @implements {ReactiveController}
 | 
						|
 */
 | 
						|
export class SyncedTabsController {
 | 
						|
  /**
 | 
						|
   * @type {boolean}
 | 
						|
   */
 | 
						|
  contextMenu;
 | 
						|
  currentSetupStateIndex = -1;
 | 
						|
  currentSyncedTabs = [];
 | 
						|
  devices = [];
 | 
						|
  /**
 | 
						|
   * The current error state as determined by `SyncedTabsErrorHandler`.
 | 
						|
   *
 | 
						|
   * @type {number}
 | 
						|
   */
 | 
						|
  errorState = null;
 | 
						|
  /**
 | 
						|
   * Component associated with this controller.
 | 
						|
   *
 | 
						|
   * @type {ReactiveControllerHost}
 | 
						|
   */
 | 
						|
  host;
 | 
						|
  /**
 | 
						|
   * @type {Function}
 | 
						|
   */
 | 
						|
  pairDeviceCallback;
 | 
						|
  searchQuery = "";
 | 
						|
  /**
 | 
						|
   * @type {Function}
 | 
						|
   */
 | 
						|
  signupCallback;
 | 
						|
 | 
						|
  /**
 | 
						|
   * Construct a new SyncedTabsController.
 | 
						|
   *
 | 
						|
   * @param {ReactiveControllerHost} host
 | 
						|
   * @param {object} options
 | 
						|
   * @param {boolean} [options.contextMenu]
 | 
						|
   *   Whether synced tab items have a secondary context menu.
 | 
						|
   * @param {Function} [options.pairDeviceCallback]
 | 
						|
   *   The function to call when the pair device window is opened.
 | 
						|
   * @param {Function} [options.signupCallback]
 | 
						|
   *   The function to call when the signup window is opened.
 | 
						|
   */
 | 
						|
  constructor(host, { contextMenu, pairDeviceCallback, signupCallback } = {}) {
 | 
						|
    this.contextMenu = contextMenu;
 | 
						|
    this.pairDeviceCallback = pairDeviceCallback;
 | 
						|
    this.signupCallback = signupCallback;
 | 
						|
    this.observe = this.observe.bind(this);
 | 
						|
    this.host = host;
 | 
						|
    this.host.addController(this);
 | 
						|
  }
 | 
						|
 | 
						|
  hostConnected() {
 | 
						|
    this.host.addEventListener("click", this);
 | 
						|
  }
 | 
						|
 | 
						|
  hostDisconnected() {
 | 
						|
    this.host.removeEventListener("click", this);
 | 
						|
  }
 | 
						|
 | 
						|
  addSyncObservers() {
 | 
						|
    Services.obs.addObserver(this.observe, SYNCED_TABS_CHANGED);
 | 
						|
    Services.obs.addObserver(this.observe, TOPIC_SETUPSTATE_CHANGED);
 | 
						|
  }
 | 
						|
 | 
						|
  removeSyncObservers() {
 | 
						|
    Services.obs.removeObserver(this.observe, SYNCED_TABS_CHANGED);
 | 
						|
    Services.obs.removeObserver(this.observe, TOPIC_SETUPSTATE_CHANGED);
 | 
						|
  }
 | 
						|
 | 
						|
  handleEvent(event) {
 | 
						|
    if (event.type == "click" && event.target.dataset.action) {
 | 
						|
      const { ErrorType } = SyncedTabsErrorHandler;
 | 
						|
      switch (event.target.dataset.action) {
 | 
						|
        case `${ErrorType.SYNC_ERROR}`:
 | 
						|
        case `${ErrorType.NETWORK_OFFLINE}`:
 | 
						|
        case `${ErrorType.PASSWORD_LOCKED}`: {
 | 
						|
          TabsSetupFlowManager.tryToClearError();
 | 
						|
          break;
 | 
						|
        }
 | 
						|
        case `${ErrorType.SIGNED_OUT}`:
 | 
						|
        case "sign-in": {
 | 
						|
          TabsSetupFlowManager.openFxASignup(event.target.ownerGlobal);
 | 
						|
          this.signupCallback?.();
 | 
						|
          break;
 | 
						|
        }
 | 
						|
        case "add-device": {
 | 
						|
          TabsSetupFlowManager.openFxAPairDevice(event.target.ownerGlobal);
 | 
						|
          this.pairDeviceCallback?.();
 | 
						|
          break;
 | 
						|
        }
 | 
						|
        case "sync-tabs-disabled": {
 | 
						|
          TabsSetupFlowManager.syncOpenTabs(event.target);
 | 
						|
          break;
 | 
						|
        }
 | 
						|
        case `${ErrorType.SYNC_DISCONNECTED}`: {
 | 
						|
          const win = event.target.ownerGlobal;
 | 
						|
          const { switchToTabHavingURI } =
 | 
						|
            win.docShell.chromeEventHandler.ownerGlobal;
 | 
						|
          switchToTabHavingURI(
 | 
						|
            "about:preferences?action=choose-what-to-sync#sync",
 | 
						|
            true,
 | 
						|
            {}
 | 
						|
          );
 | 
						|
          break;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async observe(_, topic, errorState) {
 | 
						|
    if (topic == TOPIC_SETUPSTATE_CHANGED) {
 | 
						|
      await this.updateStates(errorState);
 | 
						|
    }
 | 
						|
    if (topic == SYNCED_TABS_CHANGED) {
 | 
						|
      await this.getSyncedTabData();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  async updateStates(errorState) {
 | 
						|
    let stateIndex = TabsSetupFlowManager.uiStateIndex;
 | 
						|
    errorState = errorState || SyncedTabsErrorHandler.getErrorType();
 | 
						|
 | 
						|
    if (stateIndex == 4 && this.currentSetupStateIndex !== stateIndex) {
 | 
						|
      // trigger an initial request for the synced tabs list
 | 
						|
      await this.getSyncedTabData();
 | 
						|
    }
 | 
						|
 | 
						|
    this.currentSetupStateIndex = stateIndex;
 | 
						|
    this.errorState = errorState;
 | 
						|
    this.host.requestUpdate();
 | 
						|
  }
 | 
						|
 | 
						|
  actionMappings = {
 | 
						|
    "sign-in": {
 | 
						|
      header: "firefoxview-syncedtabs-signin-header",
 | 
						|
      description: "firefoxview-syncedtabs-signin-description",
 | 
						|
      buttonLabel: "firefoxview-syncedtabs-signin-primarybutton",
 | 
						|
    },
 | 
						|
    "add-device": {
 | 
						|
      header: "firefoxview-syncedtabs-adddevice-header",
 | 
						|
      description: "firefoxview-syncedtabs-adddevice-description",
 | 
						|
      buttonLabel: "firefoxview-syncedtabs-adddevice-primarybutton",
 | 
						|
      descriptionLink: {
 | 
						|
        name: "url",
 | 
						|
        url: "https://support.mozilla.org/kb/how-do-i-set-sync-my-computer#w_connect-additional-devices-to-sync",
 | 
						|
      },
 | 
						|
    },
 | 
						|
    "sync-tabs-disabled": {
 | 
						|
      header: "firefoxview-syncedtabs-synctabs-header",
 | 
						|
      description: "firefoxview-syncedtabs-synctabs-description",
 | 
						|
      buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton",
 | 
						|
    },
 | 
						|
    loading: {
 | 
						|
      header: "firefoxview-syncedtabs-loading-header",
 | 
						|
      description: "firefoxview-syncedtabs-loading-description",
 | 
						|
    },
 | 
						|
  };
 | 
						|
 | 
						|
  #getMessageCardForState({ error = false, action, errorState }) {
 | 
						|
    errorState = errorState || this.errorState;
 | 
						|
    let header,
 | 
						|
      description,
 | 
						|
      descriptionLink,
 | 
						|
      buttonLabel,
 | 
						|
      headerIconUrl,
 | 
						|
      mainImageUrl;
 | 
						|
    let descriptionArray;
 | 
						|
    if (error) {
 | 
						|
      let link;
 | 
						|
      ({ header, description, link, buttonLabel } =
 | 
						|
        SyncedTabsErrorHandler.getFluentStringsForErrorType(errorState));
 | 
						|
      action = `${errorState}`;
 | 
						|
      headerIconUrl = "chrome://global/skin/icons/info-filled.svg";
 | 
						|
      mainImageUrl =
 | 
						|
        "chrome://browser/content/firefoxview/synced-tabs-error.svg";
 | 
						|
      descriptionArray = [description];
 | 
						|
      if (errorState == "password-locked") {
 | 
						|
        descriptionLink = {};
 | 
						|
        // This is ugly, but we need to special case this link so we can
 | 
						|
        // coexist with the old view.
 | 
						|
        descriptionArray.push("firefoxview-syncedtab-password-locked-link");
 | 
						|
        descriptionLink.name = "syncedtab-password-locked-link";
 | 
						|
        descriptionLink.url = link.href;
 | 
						|
      }
 | 
						|
    } else {
 | 
						|
      header = this.actionMappings[action].header;
 | 
						|
      description = this.actionMappings[action].description;
 | 
						|
      buttonLabel = this.actionMappings[action].buttonLabel;
 | 
						|
      descriptionLink = this.actionMappings[action].descriptionLink;
 | 
						|
      mainImageUrl =
 | 
						|
        "chrome://browser/content/firefoxview/synced-tabs-error.svg";
 | 
						|
      descriptionArray = [description];
 | 
						|
    }
 | 
						|
    return {
 | 
						|
      action,
 | 
						|
      buttonLabel,
 | 
						|
      descriptionArray,
 | 
						|
      descriptionLink,
 | 
						|
      error,
 | 
						|
      header,
 | 
						|
      headerIconUrl,
 | 
						|
      mainImageUrl,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  getRenderInfo() {
 | 
						|
    let renderInfo = {};
 | 
						|
    for (let tab of this.currentSyncedTabs) {
 | 
						|
      if (!(tab.client in renderInfo)) {
 | 
						|
        renderInfo[tab.client] = {
 | 
						|
          name: tab.device,
 | 
						|
          deviceType: tab.deviceType,
 | 
						|
          tabs: [],
 | 
						|
        };
 | 
						|
      }
 | 
						|
      renderInfo[tab.client].tabs.push(tab);
 | 
						|
    }
 | 
						|
 | 
						|
    // Add devices without tabs
 | 
						|
    for (let device of this.devices) {
 | 
						|
      if (!(device.id in renderInfo)) {
 | 
						|
        renderInfo[device.id] = {
 | 
						|
          name: device.name,
 | 
						|
          deviceType: device.clientType,
 | 
						|
          tabs: [],
 | 
						|
        };
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    for (let id in renderInfo) {
 | 
						|
      renderInfo[id].tabItems = this.searchQuery
 | 
						|
        ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs))
 | 
						|
        : this.getTabItems(renderInfo[id].tabs);
 | 
						|
    }
 | 
						|
    return renderInfo;
 | 
						|
  }
 | 
						|
 | 
						|
  getMessageCard() {
 | 
						|
    switch (this.currentSetupStateIndex) {
 | 
						|
      case 0 /* error-state */:
 | 
						|
        if (this.errorState) {
 | 
						|
          return this.#getMessageCardForState({ error: true });
 | 
						|
        }
 | 
						|
        return this.#getMessageCardForState({ action: "loading" });
 | 
						|
      case 1 /* not-signed-in */:
 | 
						|
        if (Services.prefs.prefHasUserValue("services.sync.lastversion")) {
 | 
						|
          // If this pref is set, the user has signed out of sync.
 | 
						|
          // This path is also taken if we are disconnected from sync. See bug 1784055
 | 
						|
          return this.#getMessageCardForState({
 | 
						|
            error: true,
 | 
						|
            errorState: "signed-out",
 | 
						|
          });
 | 
						|
        }
 | 
						|
        return this.#getMessageCardForState({ action: "sign-in" });
 | 
						|
      case 2 /* connect-secondary-device*/:
 | 
						|
        return this.#getMessageCardForState({ action: "add-device" });
 | 
						|
      case 3 /* disabled-tab-sync */:
 | 
						|
        return this.#getMessageCardForState({ action: "sync-tabs-disabled" });
 | 
						|
      case 4 /* synced-tabs-loaded*/:
 | 
						|
        // There seems to be an edge case where sync says everything worked
 | 
						|
        // fine but we have no devices.
 | 
						|
        if (!this.devices.length) {
 | 
						|
          return this.#getMessageCardForState({ action: "add-device" });
 | 
						|
        }
 | 
						|
    }
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  getTabItems(tabs) {
 | 
						|
    return tabs?.map(tab => ({
 | 
						|
      icon: tab.icon,
 | 
						|
      title: tab.title,
 | 
						|
      time: tab.lastUsed * 1000,
 | 
						|
      url: tab.url,
 | 
						|
      primaryL10nId: "firefoxview-tabs-list-tab-button",
 | 
						|
      primaryL10nArgs: JSON.stringify({ targetURI: tab.url }),
 | 
						|
      secondaryL10nId: this.contextMenu
 | 
						|
        ? "fxviewtabrow-options-menu-button"
 | 
						|
        : undefined,
 | 
						|
      secondaryL10nArgs: this.contextMenu
 | 
						|
        ? JSON.stringify({ tabTitle: tab.title })
 | 
						|
        : undefined,
 | 
						|
    }));
 | 
						|
  }
 | 
						|
 | 
						|
  updateTabsList(syncedTabs) {
 | 
						|
    if (!syncedTabs.length) {
 | 
						|
      this.currentSyncedTabs = syncedTabs;
 | 
						|
    }
 | 
						|
 | 
						|
    const tabsToRender = syncedTabs;
 | 
						|
 | 
						|
    // Return early if new tabs are the same as previous ones
 | 
						|
    if (lazy.ObjectUtils.deepEqual(tabsToRender, this.currentSyncedTabs)) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.currentSyncedTabs = tabsToRender;
 | 
						|
    this.host.requestUpdate();
 | 
						|
  }
 | 
						|
 | 
						|
  async getSyncedTabData() {
 | 
						|
    this.devices = await lazy.SyncedTabs.getTabClients();
 | 
						|
    let tabs = await lazy.SyncedTabs.createRecentTabsList(this.devices, 50, {
 | 
						|
      removeAllDupes: false,
 | 
						|
      removeDeviceDupes: true,
 | 
						|
    });
 | 
						|
 | 
						|
    this.updateTabsList(tabs);
 | 
						|
  }
 | 
						|
}
 |