Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/widget/tests/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 1 kB image not shown  

Quelle  browser-sync.js   Sprache: unbekannt

 
/* 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/. */


// This file is loaded into the browser window scope.
/* eslint-env mozilla/browser-window */

const {
  FX_MONITOR_OAUTH_CLIENT_ID,
  FX_RELAY_OAUTH_CLIENT_ID,
  VPN_OAUTH_CLIENT_ID,
} = ChromeUtils.importESModule(
  "resource://gre/modules/FxAccountsCommon.sys.mjs"
);

const { UIState } = ChromeUtils.importESModule(
  "resource://services-sync/UIState.sys.mjs"
);

ChromeUtils.defineESModuleGetters(this, {
  ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs",
  EnsureFxAccountsWebChannel:
    "resource://gre/modules/FxAccountsWebChannel.sys.mjs",

  ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
  FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
  MenuMessage: "resource:///modules/asrouter/MenuMessage.sys.mjs",
  SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
  SyncedTabsManagement: "resource://services-sync/SyncedTabs.sys.mjs",
  Weave: "resource://services-sync/main.sys.mjs",
});

const MIN_STATUS_ANIMATION_DURATION = 1600;

this.SyncedTabsPanelList = class SyncedTabsPanelList {
  static sRemoteTabsDeckIndices = {
    DECKINDEX_TABS: 0,
    DECKINDEX_FETCHING: 1,
    DECKINDEX_TABSDISABLED: 2,
    DECKINDEX_NOCLIENTS: 3,
  };

  static sRemoteTabsPerPage = 25;
  static sRemoteTabsNextPageMinTabs = 5;

  constructor(panelview, deck, tabsList, separator) {
    this.QueryInterface = ChromeUtils.generateQI([
      "nsIObserver",
      "nsISupportsWeakReference",
    ]);

    Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED, true);
    this.deck = deck;
    this.tabsList = tabsList;
    this.separator = separator;
    this._showSyncedTabsPromise = Promise.resolve();

    this.createSyncedTabs();
  }

  observe(subject, topic) {
    if (topic == SyncedTabs.TOPIC_TABS_CHANGED) {
      this._showSyncedTabs();
    }
  }

  createSyncedTabs() {
    if (SyncedTabs.isConfiguredToSyncTabs) {
      if (SyncedTabs.hasSyncedThisSession) {
        this.deck.selectedIndex =
          SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABS;
      } else {
        // Sync hasn't synced tabs yet, so show the "fetching" panel.
        this.deck.selectedIndex =
          SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_FETCHING;
      }
      // force a background sync.
      SyncedTabs.syncTabs().catch(ex => {
        console.error(ex);
      });
      this.deck.toggleAttribute("syncingtabs"true);
      // show the current list - it will be updated by our observer.
      this._showSyncedTabs();
      if (this.separator) {
        this.separator.hidden = false;
      }
    } else {
      // not configured to sync tabs, so no point updating the list.
      this.deck.selectedIndex =
        SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABSDISABLED;
      this.deck.toggleAttribute("syncingtabs"false);
      if (this.separator) {
        this.separator.hidden = true;
      }
    }
  }

  // Update the synced tab list after any existing in-flight updates are complete.
  _showSyncedTabs(paginationInfo) {
    this._showSyncedTabsPromise = this._showSyncedTabsPromise.then(
      () => {
        return this.__showSyncedTabs(paginationInfo);
      },
      e => {
        console.error(e);
      }
    );
  }

  // Return a new promise to update the tab list.
  __showSyncedTabs(paginationInfo) {
    if (!this.tabsList) {
      // Closed between the previous `this._showSyncedTabsPromise`
      // resolving and now.
      return undefined;
    }
    return SyncedTabs.getTabClients()
      .then(clients => {
        let noTabs = !UIState.get().syncEnabled || !clients.length;
        this.deck.toggleAttribute("syncingtabs", !noTabs);
        if (this.separator) {
          this.separator.hidden = noTabs;
        }

        // The view may have been hidden while the promise was resolving.
        if (!this.tabsList) {
          return;
        }
        if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) {
          // the "fetching tabs" deck is being shown - let's leave it there.
          // When that first sync completes we'll be notified and update.
          return;
        }

        if (clients.length === 0) {
          this.deck.selectedIndex =
            SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_NOCLIENTS;
          return;
        }
        this.deck.selectedIndex =
          SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABS;
        this._clearSyncedTabList();
        SyncedTabs.sortTabClientsByLastUsed(clients);
        let fragment = document.createDocumentFragment();

        let clientNumber = 0;
        for (let client of clients) {
          // add a menu separator for all clients other than the first.
          if (fragment.lastElementChild) {
            let separator = document.createXULElement("toolbarseparator");
            fragment.appendChild(separator);
          }
          // We add the client's elements to a container, and indicate which
          // element labels it.
          let labelId = `synced-tabs-client-${clientNumber++}`;
          let container = document.createXULElement("vbox");
          container.classList.add("PanelUI-remotetabs-clientcontainer");
          container.setAttribute("role""group");
          container.setAttribute("aria-labelledby", labelId);
          let clientPaginationInfo =
            paginationInfo && paginationInfo.clientId == client.id
              ? paginationInfo
              : { clientId: client.id };
          this._appendSyncClient(
            client,
            container,
            labelId,
            clientPaginationInfo
          );
          fragment.appendChild(container);
        }
        this.tabsList.appendChild(fragment);
      })
      .catch(err => {
        console.error(err);
      })
      .then(() => {
        // an observer for tests.
        Services.obs.notifyObservers(
          null,
          "synced-tabs-menu:test:tabs-updated"
        );
      });
  }

  _clearSyncedTabList() {
    let list = this.tabsList;
    while (list.lastChild) {
      list.lastChild.remove();
    }
  }

  _createNoSyncedTabsElement(messageAttr, appendTo = null) {
    if (!appendTo) {
      appendTo = this.tabsList;
    }

    let messageLabel = document.createXULElement("label");
    document.l10n.setAttributes(
      messageLabel,
      this.tabsList.getAttribute(messageAttr)
    );
    appendTo.appendChild(messageLabel);
    return messageLabel;
  }

  _appendSyncClient(client, container, labelId, paginationInfo) {
    let { maxTabs = SyncedTabsPanelList.sRemoteTabsPerPage } = paginationInfo;
    // Create the element for the remote client.
    let clientItem = document.createXULElement("label");
    clientItem.setAttribute("id", labelId);
    clientItem.setAttribute("itemtype""client");
    clientItem.setAttribute(
      "tooltiptext",
      gSync.fluentStrings.formatValueSync("appmenu-fxa-last-sync", {
        time: gSync.formatLastSyncDate(new Date(client.lastModified)),
      })
    );
    clientItem.textContent = client.name;

    container.appendChild(clientItem);

    if (!client.tabs.length) {
      let label = this._createNoSyncedTabsElement(
        "notabsforclientlabel",
        container
      );
      label.setAttribute("class""PanelUI-remotetabs-notabsforclient-label");
    } else {
      // We have the client obj but we need the FxA device obj so we use the clients
      // engine to get us the FxA device
      let device =
        fxAccounts.device.recentDeviceList &&
        fxAccounts.device.recentDeviceList.find(
          d =>
            d.id === Weave.Service.clientsEngine.getClientFxaDeviceId(client.id)
        );
      let remoteTabCloseAvailable =
        device && fxAccounts.commands.closeTab.isDeviceCompatible(device);

      let tabs = client.tabs.filter(t => !t.inactive);
      let hasInactive = tabs.length != client.tabs.length;

      if (hasInactive) {
        container.append(this._createShowInactiveTabsElement(client, device));
      }
      // If this page isn't displaying all (regular, active) tabs, show a "Show More" button.
      let hasNextPage = tabs.length > maxTabs;
      let nextPageIsLastPage =
        hasNextPage &&
        maxTabs + SyncedTabsPanelList.sRemoteTabsPerPage >= tabs.length;
      if (nextPageIsLastPage) {
        // When the user clicks "Show More", try to have at least sRemoteTabsNextPageMinTabs more tabs
        // to display in order to avoid user frustration
        maxTabs = Math.min(
          tabs.length - SyncedTabsPanelList.sRemoteTabsNextPageMinTabs,
          maxTabs
        );
      }
      if (hasNextPage) {
        tabs = tabs.slice(0, maxTabs);
      }
      for (let [index, tab] of tabs.entries()) {
        let tabEnt = this._createSyncedTabElement(
          tab,
          index,
          device,
          remoteTabCloseAvailable
        );
        container.appendChild(tabEnt);
      }
      if (hasNextPage) {
        let showAllEnt = this._createShowMoreSyncedTabsElement(paginationInfo);
        container.appendChild(showAllEnt);
      }
    }
  }

  _createSyncedTabElement(tabInfo, index, device, canCloseTabs) {
    let tabContainer = document.createXULElement("hbox");
    tabContainer.setAttribute(
      "class",
      "PanelUI-tabitem-container all-tabs-item"
    );

    let item = document.createXULElement("toolbarbutton");
    let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
    item.setAttribute("itemtype""tab");
    item.classList.add(
      "all-tabs-button",
      "subviewbutton",
      "subviewbutton-iconic"
    );
    item.setAttribute("targetURI", tabInfo.url);
    item.setAttribute(
      "label",
      tabInfo.title != "" ? tabInfo.title : tabInfo.url
    );
    if (tabInfo.icon) {
      item.setAttribute("image", tabInfo.icon);
    }
    item.setAttribute("tooltiptext", tooltipText);
    // We need to use "click" instead of "command" here so openUILink
    // respects different buttons (eg, to open in a new tab).
    item.addEventListener("click", e => {
      // We want to differentiate between when the fxa panel is within the app menu/hamburger bar
      let object = window.gSync._getEntryPointForElement(e.currentTarget);
      SyncedTabs.recordSyncedTabsTelemetry(object, "click", {
        tab_pos: index.toString(),
      });
      document.defaultView.openUILink(tabInfo.url, e, {
        triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
          {}
        ),
      });
      if (BrowserUtils.whereToOpenLink(e) != "current") {
        e.preventDefault();
        e.stopPropagation();
      } else {
        CustomizableUI.hidePanelForNode(item);
      }
    });
    tabContainer.appendChild(item);
    // We should only add an X button next to tabs if the device
    // is broadcasting that it can remotely close tabs
    if (canCloseTabs) {
      let closeBtn = this._createCloseTabElement(tabInfo.url, device);
      closeBtn.tab = item;
      tabContainer.appendChild(closeBtn);
      let undoBtn = this._createUndoCloseTabElement(tabInfo.url, device);
      undoBtn.tab = item;
      tabContainer.appendChild(undoBtn);
    }
    return tabContainer;
  }

  _createShowMoreSyncedTabsElement(paginationInfo) {
    let showMoreItem = document.createXULElement("toolbarbutton");
    showMoreItem.setAttribute("itemtype""showmorebutton");
    showMoreItem.setAttribute("closemenu""none");
    showMoreItem.classList.add("subviewbutton""subviewbutton-nav-down");
    document.l10n.setAttributes(showMoreItem, "appmenu-remote-tabs-showmore");

    paginationInfo.maxTabs = Infinity;
    showMoreItem.addEventListener("click", e => {
      e.preventDefault();
      e.stopPropagation();
      this._showSyncedTabs(paginationInfo);
    });
    return showMoreItem;
  }

  _createShowInactiveTabsElement(client, device) {
    let showItem = document.createXULElement("toolbarbutton");
    showItem.setAttribute("itemtype""showinactivebutton");
    showItem.setAttribute("closemenu""none");
    showItem.classList.add("subviewbutton""subviewbutton-nav");
    document.l10n.setAttributes(
      showItem,
      "appmenu-remote-tabs-show-inactive-tabs"
    );

    let canClose =
      device && fxAccounts.commands.closeTab.isDeviceCompatible(device);

    showItem.addEventListener("click", e => {
      let node = PanelMultiView.getViewNode(
        document,
        "PanelUI-fxa-menu-inactive-tabs"
      );

      // device name.
      let label = node.querySelector("label[itemtype='client']");
      label.textContent = client.name;

      // Update the tab list.
      let container = node.querySelector(".panel-subview-body");
      container.replaceChildren(
        ...client.tabs
          .filter(t => t.inactive)
          .map((tab, index) =>
            this._createSyncedTabElement(tab, index, device, canClose)
          )
      );
      PanelUI.showSubView("PanelUI-fxa-menu-inactive-tabs", showItem, e);
    });
    return showItem;
  }

  _createCloseTabElement(url, device) {
    let closeBtn = document.createXULElement("toolbarbutton");
    closeBtn.classList.add(
      "remote-tabs-close-button",
      "all-tabs-close-button",
      "subviewbutton"
    );
    closeBtn.setAttribute("closemenu""none");
    closeBtn.setAttribute(
      "tooltiptext",
      gSync.fluentStrings.formatValueSync("synced-tabs-context-close-tab", {
        deviceName: device.name,
      })
    );
    closeBtn.addEventListener("click", e => {
      e.stopPropagation();

      let tabContainer = closeBtn.parentNode;
      let tabList = tabContainer.parentNode;

      let undoBtn = tabContainer.querySelector(".remote-tabs-undo-button");

      let prevClose = tabList.querySelector(
        ".remote-tabs-undo-button:not([hidden])"
      );
      if (prevClose) {
        let prevCloseContainer = prevClose.parentNode;
        prevCloseContainer.classList.add("tabitem-removed");
        prevCloseContainer.addEventListener("transitionend", () => {
          prevCloseContainer.remove();
        });
      }
      closeBtn.hidden = true;
      undoBtn.hidden = false;
      // This tab has been closed so we prevent the user from
      // interacting with it
      if (closeBtn.tab) {
        closeBtn.tab.disabled = true;
      }
      // The user could be hitting multiple tabs across multiple devices, with a few
      // seconds in-between -- we should not immediately fire off pushes, so we
      // add it to a queue and send in bulk at a later time
      SyncedTabsManagement.enqueueTabToClose(device.id, url);
    });
    return closeBtn;
  }

  _createUndoCloseTabElement(url, device) {
    let undoBtn = document.createXULElement("toolbarbutton");
    undoBtn.classList.add("remote-tabs-undo-button""subviewbutton");
    undoBtn.setAttribute("closemenu""none");
    undoBtn.setAttribute("data-l10n-id""text-action-undo");
    undoBtn.hidden = true;

    undoBtn.addEventListener("click"function (e) {
      e.stopPropagation();

      undoBtn.hidden = true;
      let closeBtn = undoBtn.parentNode.querySelector(".all-tabs-close-button");
      closeBtn.hidden = false;
      if (undoBtn.tab) {
        undoBtn.tab.disabled = false;
      }

      // remove this tab from being remotely closed
      SyncedTabsManagement.removePendingTabToClose(device.id, url);
    });
    return undoBtn;
  }

  destroy() {
    Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
    this.tabsList = null;
    this.deck = null;
    this.separator = null;
  }
};

var gSync = {
  _initialized: false,
  _isCurrentlySyncing: false,
  // The last sync start time. Used to calculate the leftover animation time
  // once syncing completes (bug 1239042).
  _syncStartTime: 0,
  _syncAnimationTimer: 0,
  _obs: ["weave:engine:sync:finish""quit-application", UIState.ON_UPDATE],

  get log() {
    if (!this._log) {
      const { Log } = ChromeUtils.importESModule(
        "resource://gre/modules/Log.sys.mjs"
      );
      let syncLog = Log.repository.getLogger("Sync.Browser");
      syncLog.manageLevelFromPref("services.sync.log.logger.browser");
      this._log = syncLog;
    }
    return this._log;
  },

  get fluentStrings() {
    delete this.fluentStrings;
    return (this.fluentStrings = new Localization(
      [
        "branding/brand.ftl",
        "browser/accounts.ftl",
        "browser/appmenu.ftl",
        "browser/sync.ftl",
        "browser/syncedTabs.ftl",
        "browser/newtab/asrouter.ftl",
      ],
      true
    ));
  },

  // Returns true if FxA is configured, but the send tab targets list isn't
  // ready yet.
  get sendTabConfiguredAndLoading() {
    return (
      UIState.get().status == UIState.STATUS_SIGNED_IN &&
      !fxAccounts.device.recentDeviceList
    );
  },

  get isSignedIn() {
    return UIState.get().status == UIState.STATUS_SIGNED_IN;
  },

  shouldHideSendContextMenuItems(enabled) {
    const state = UIState.get();
    // Only show the "Send..." context menu items when sending would be possible
    if (
      enabled &&
      state.status == UIState.STATUS_SIGNED_IN &&
      state.syncEnabled &&
      this.getSendTabTargets().length
    ) {
      return false;
    }
    return true;
  },

  getSendTabTargets() {
    const targets = [];
    if (
      UIState.get().status != UIState.STATUS_SIGNED_IN ||
      !fxAccounts.device.recentDeviceList
    ) {
      return targets;
    }
    for (let d of fxAccounts.device.recentDeviceList) {
      if (d.isCurrentDevice) {
        continue;
      }

      if (fxAccounts.commands.sendTab.isDeviceCompatible(d)) {
        targets.push(d);
      }
    }
    return targets.sort((a, b) => b.lastAccessTime - a.lastAccessTime);
  },

  _definePrefGetters() {
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "FXA_ENABLED",
      "identity.fxaccounts.enabled"
    );
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "FXA_CTA_MENU_ENABLED",
      "identity.fxaccounts.toolbar.pxiToolbarEnabled"
    );
  },

  maybeUpdateUIState() {
    // Update the UI.
    if (UIState.isReady()) {
      const state = UIState.get();
      // If we are not configured, the UI is already in the right state when
      // we open the window. We can avoid a repaint.
      if (state.status != UIState.STATUS_NOT_CONFIGURED) {
        this.updateAllUI(state);
      }
    }
  },

  init() {
    if (this._initialized) {
      return;
    }

    this._definePrefGetters();

    if (!this.FXA_ENABLED) {
      this.onFxaDisabled();
      return;
    }

    MozXULElement.insertFTLIfNeeded("browser/sync.ftl");
    MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl");

    // Label for the sync buttons.
    const appMenuLabel = PanelMultiView.getViewNode(
      document,
      "appMenu-fxa-label2"
    );
    if (!appMenuLabel) {
      // We are in a window without our elements - just abort now, without
      // setting this._initialized, so we don't attempt to remove observers.
      return;
    }
    // We start with every menuitem hidden (except for the "setup sync" state),
    // so that we don't need to init the sync UI on windows like pageInfo.xhtml
    // (see bug 1384856).
    // maybeUpdateUIState() also optimizes for this - if we should be in the
    // "setup sync" state, that function assumes we are already in it and
    // doesn't re-initialize the UI elements.
    document.getElementById("sync-setup").hidden = false;
    PanelMultiView.getViewNode(
      document,
      "PanelUI-remotetabs-setupsync"
    ).hidden = false;

    const appMenuHeaderTitle = PanelMultiView.getViewNode(
      document,
      "appMenu-header-title"
    );
    const appMenuHeaderDescription = PanelMultiView.getViewNode(
      document,
      "appMenu-header-description"
    );
    const appMenuHeaderText = PanelMultiView.getViewNode(
      document,
      "appMenu-fxa-text"
    );
    appMenuHeaderTitle.hidden = true;
    // We must initialize the label attribute here instead of the markup
    // due to a timing error. The fluent label attribute was being applied
    // after we had updated appMenuLabel and thus displayed an incorrect
    // label for signed in users.
    const [headerDesc, headerText] = this.fluentStrings.formatValuesSync([
      "appmenu-fxa-signed-in-label",
      "appmenu-fxa-sync-and-save-data2",
    ]);
    appMenuHeaderDescription.value = headerDesc;
    appMenuHeaderText.textContent = headerText;

    for (let topic of this._obs) {
      Services.obs.addObserver(this, topic, true);
    }

    this.maybeUpdateUIState();

    EnsureFxAccountsWebChannel();

    let fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa");
    fxaPanelView.addEventListener("ViewShowing"this);
    fxaPanelView.addEventListener("ViewHiding"this);
    fxaPanelView.addEventListener("command"this);
    PanelMultiView.getViewNode(
      document,
      "PanelUI-fxa-menu-syncnow-button"
    ).addEventListener("mouseover"this);
    PanelMultiView.getViewNode(
      document,
      "PanelUI-fxa-menu-sendtab-not-configured-button"
    ).addEventListener("command"this);
    PanelMultiView.getViewNode(
      document,
      "PanelUI-fxa-menu-sendtab-connect-device-button"
    ).addEventListener("command"this);

    PanelUI.mainView.addEventListener("ViewShowing"this);

    // If the experiment is enabled, we'll need to update the panels
    // to show some different text to the user
    if (this.FXA_CTA_MENU_ENABLED) {
      this.updateFxAPanel(UIState.get());
      this.updateCTAPanel();
    }

    const avatarIconVariant =
      NimbusFeatures.fxaButtonVisibility.getVariable("avatarIconVariant");
    if (avatarIconVariant) {
      this.applyAvatarIconVariant(avatarIconVariant);
    }

    this._initialized = true;
  },

  uninit() {
    if (!this._initialized) {
      return;
    }

    for (let topic of this._obs) {
      Services.obs.removeObserver(this, topic);
    }

    this._initialized = false;
  },

  handleEvent(event) {
    switch (event.type) {
      case "mouseover":
        this.refreshSyncButtonsTooltip();
        break;
      case "command": {
        this.onCommand(event.target);
        break;
      }
      case "ViewShowing": {
        if (event.target == PanelUI.mainView) {
          this.onAppMenuShowing();
        } else {
          this.onFxAPanelViewShowing(event.target);
        }
        break;
      }
      case "ViewHiding": {
        this.onFxAPanelViewHiding(event.target);
      }
    }
  },

  onAppMenuShowing() {
    const appMenuHeaderText = PanelMultiView.getViewNode(
      document,
      "appMenu-fxa-text"
    );

    const ctaDefaultStringID = "appmenu-fxa-sync-and-save-data2";
    const ctaStringID = this.getMenuCtaCopy(NimbusFeatures.fxaAppMenuItem);

    document.l10n.setAttributes(
      appMenuHeaderText,
      ctaStringID || ctaDefaultStringID
    );

    if (NimbusFeatures.fxaAppMenuItem.getVariable("ctaCopyVariant")) {
      NimbusFeatures.fxaAppMenuItem.recordExposureEvent();
    }
  },

  onFxAPanelViewShowing(panelview) {
    let messageId = panelview.getAttribute(
      MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR
    );
    if (messageId) {
      MenuMessage.recordMenuMessageTelemetry(
        "IMPRESSION",
        MenuMessage.SOURCES.PXI_MENU,
        messageId
      );
      let message = ASRouter.getMessageById(messageId);
      ASRouter.addImpression(message);
    }

    let syncNowBtn = panelview.querySelector(".syncnow-label");
    let l10nId = syncNowBtn.getAttribute(
      this._isCurrentlySyncing
        ? "syncing-data-l10n-id"
        : "sync-now-data-l10n-id"
    );
    document.l10n.setAttributes(syncNowBtn, l10nId);

    // This needs to exist because if the user is signed in
    // but the user disabled or disconnected sync we should not show the button
    const syncPrefsButtonEl = PanelMultiView.getViewNode(
      document,
      "PanelUI-fxa-menu-sync-prefs-button"
    );
    const syncEnabled = UIState.get().syncEnabled;
    syncPrefsButtonEl.hidden = !syncEnabled;
    if (!syncEnabled) {
      this._disableSyncOffIndicator();
    }

    // We should ensure that we do not show the sign out button
    // if the user is not signed in
    const signOutButtonEl = PanelMultiView.getViewNode(
      document,
      "PanelUI-fxa-menu-account-signout-button"
    );
    signOutButtonEl.hidden = !this.isSignedIn;

    panelview.syncedTabsPanelList = new SyncedTabsPanelList(
      panelview,
      PanelMultiView.getViewNode(document, "PanelUI-fxa-remotetabs-deck"),
      PanelMultiView.getViewNode(document, "PanelUI-fxa-remotetabs-tabslist"),
      PanelMultiView.getViewNode(document, "PanelUI-remote-tabs-separator")
    );

    // Any variant on the CTA will have been applied inside of updateFxAPanel,
    // but now that the panel is showing, we record exposure.
    const ctaCopyVariant =
      NimbusFeatures.fxaAvatarMenuItem.getVariable("ctaCopyVariant");
    if (ctaCopyVariant) {
      NimbusFeatures.fxaAvatarMenuItem.recordExposureEvent();
    }

    // We want to record exposure if the user has sync disabled and has
    // clicked to open the FxA panel
    if (this.isSignedIn && !UIState.get().syncEnabled) {
      NimbusFeatures.syncSetupFlow.recordExposureEvent();
    }
  },

  onFxAPanelViewHiding(panelview) {
    MenuMessage.hidePxiMenuMessage(gBrowser.selectedBrowser);
    panelview.syncedTabsPanelList.destroy();
    panelview.syncedTabsPanelList = null;
  },

  onCommand(button) {
    switch (button.id) {
      case "PanelUI-fxa-menu-sync-prefs-button":
      // fall through
      case "PanelUI-fxa-menu-setup-sync-button":
        this.openPrefsFromFxaMenu("sync_settings", button);
        break;
      case "PanelUI-fxa-menu-setup-sync-button-new":
        this.openChooseWhatToSync("sync_settings", button);
        break;

      case "PanelUI-fxa-menu-sendtab-connect-device-button":
      // fall through
      case "PanelUI-fxa-menu-connect-device-button":
        this.clickOpenConnectAnotherDevice(button);
        break;

      case "fxa-manage-account-button":
        this.clickFxAMenuHeaderButton(button);
        break;
      case "PanelUI-fxa-menu-syncnow-button":
        this.doSyncFromFxaMenu(button);
        break;
      case "PanelUI-fxa-menu-sendtab-button":
        this.showSendToDeviceViewFromFxaMenu(button);
        break;
      case "PanelUI-fxa-menu-account-signout-button":
        this.disconnect();
        break;
      case "PanelUI-fxa-menu-monitor-button":
        this.openMonitorLink(button);
        break;
      case "PanelUI-services-menu-relay-button":
      case "PanelUI-fxa-menu-relay-button":
        this.openRelayLink(button);
        break;
      case "PanelUI-fxa-menu-vpn-button":
        this.openVPNLink(button);
        break;
      case "PanelUI-fxa-menu-sendtab-not-configured-button":
        this.openPrefsFromFxaMenu("send_tab", button);
        break;
    }
  },

  observe(subject, topic, data) {
    if (!this._initialized) {
      console.error("browser-sync observer called after unload: ", topic);
      return;
    }
    switch (topic) {
      case UIState.ON_UPDATE: {
        const state = UIState.get();
        this.updateAllUI(state);
        break;
      }
      case "quit-application":
        // Stop the animation timer on shutdown, since we can't update the UI
        // after this.
        clearTimeout(this._syncAnimationTimer);
        break;
      case "weave:engine:sync:finish":
        if (data != "clients") {
          return;
        }
        this.onClientsSynced();
        this.updateFxAPanel(UIState.get());
        break;
    }
  },

  updateAllUI(state) {
    this.updatePanelPopup(state);
    this.updateState(state);
    this.updateSyncButtonsTooltip(state);
    this.updateSyncStatus(state);
    this.updateFxAPanel(state);
    this.ensureFxaDevices();
    this.fetchListOfOAuthClients();
  },

  // Ensure we have *something* in `fxAccounts.device.recentDeviceList` as some
  // of our UI logic depends on it not being null. When FxA is notified of a
  // device change it will auto refresh `recentDeviceList`, and all UI which
  // shows the device list will start with `recentDeviceList`, but should also
  // force a refresh, both of which should mean in the worst-case, the UI is up
  // to date after a very short delay.
  async ensureFxaDevices() {
    if (UIState.get().status != UIState.STATUS_SIGNED_IN) {
      console.info("Skipping device list refresh; not signed in");
      return;
    }
    if (!fxAccounts.device.recentDeviceList) {
      if (await this.refreshFxaDevices()) {
        // Assuming we made the call successfully it should be impossible to end
        // up with a falsey recentDeviceList, so make noise if that's false.
        if (!fxAccounts.device.recentDeviceList) {
          console.warn("Refreshing device list didn't find any devices.");
        }
      }
    }
  },

  // Force a refresh of the fxa device list.  Note that while it's theoretically
  // OK to call `fxAccounts.device.refreshDeviceList` multiple times concurrently
  // and regularly, this call tells it to avoid those protections, so will always
  // hit the FxA servers - therefore, you should be very careful how often you
  // call this.
  // Returns Promise<bool> to indicate whether a refresh was actually done.
  async refreshFxaDevices() {
    if (UIState.get().status != UIState.STATUS_SIGNED_IN) {
      console.info("Skipping device list refresh; not signed in");
      return false;
    }
    try {
      // Do the actual refresh telling it to avoid the "flooding" protections.
      await fxAccounts.device.refreshDeviceList({ ignoreCached: true });
      return true;
    } catch (e) {
      this.log.error("Refreshing device list failed.", e);
      return false;
    }
  },

  /**
   * Potential network call. Fetch the list of OAuth clients attached to the current Mozilla account.
   * @returns {Promise<boolean>} - Resolves to true if successful, false otherwise.
   */

  async fetchListOfOAuthClients() {
    if (!this.isSignedIn) {
      console.info("Skipping fetching other attached clients");
      return false;
    }
    try {
      this._attachedClients = await fxAccounts.listAttachedOAuthClients();
      return true;
    } catch (e) {
      this.log.error("Could not fetch attached OAuth clients", e);
      return false;
    }
  },

  updateSendToDeviceTitle() {
    const tabCount = gBrowser.selectedTab.multiselected
      ? gBrowser.selectedTabs.length
      : 1;
    document.l10n.setArgs(
      PanelMultiView.getViewNode(document, "PanelUI-fxa-menu-sendtab-button"),
      { tabCount }
    );
  },

  showSendToDeviceView(anchor) {
    PanelUI.showSubView("PanelUI-sendTabToDevice", anchor);
    let panelViewNode = document.getElementById("PanelUI-sendTabToDevice");
    this._populateSendTabToDevicesView(panelViewNode);
  },

  showSendToDeviceViewFromFxaMenu(anchor) {
    const { status } = UIState.get();
    if (status === UIState.STATUS_NOT_CONFIGURED) {
      PanelUI.showSubView("PanelUI-fxa-menu-sendtab-not-configured", anchor);
      return;
    }

    const targets = this.sendTabConfiguredAndLoading
      ? []
      : this.getSendTabTargets();
    if (!targets.length) {
      PanelUI.showSubView("PanelUI-fxa-menu-sendtab-no-devices", anchor);
      return;
    }

    this.showSendToDeviceView(anchor);
    this.emitFxaToolbarTelemetry("send_tab", anchor);
  },

  _populateSendTabToDevicesView(panelViewNode, reloadDevices = true) {
    let bodyNode = panelViewNode.querySelector(".panel-subview-body");
    let panelNode = panelViewNode.closest("panel");
    let browser = gBrowser.selectedBrowser;
    let uri = browser.currentURI;
    let title = browser.contentTitle;
    let multiselected = gBrowser.selectedTab.multiselected;

    // This is on top because it also clears the device list between state
    // changes.
    this.populateSendTabToDevicesMenu(
      bodyNode,
      uri,
      title,
      multiselected,
      (clientId, name, clientType, lastModified) => {
        if (!name) {
          return document.createXULElement("toolbarseparator");
        }
        let item = document.createXULElement("toolbarbutton");
        item.setAttribute("wrap"true);
        item.setAttribute("align""start");
        item.classList.add("sendToDevice-device""subviewbutton");
        if (clientId) {
          item.classList.add("subviewbutton-iconic");
          if (lastModified) {
            let lastSyncDate = gSync.formatLastSyncDate(lastModified);
            if (lastSyncDate) {
              item.setAttribute(
                "tooltiptext",
                this.fluentStrings.formatValueSync("appmenu-fxa-last-sync", {
                  time: lastSyncDate,
                })
              );
            }
          }
        }

        item.addEventListener("command", () => {
          if (panelNode) {
            PanelMultiView.hidePopup(panelNode);
          }
        });
        return item;
      },
      true
    );

    bodyNode.removeAttribute("state");
    // If the app just started, we won't have fetched the device list yet. Sync
    // does this automatically ~10 sec after startup, but there's no trigger for
    // this if we're signed in to FxA, but not Sync.
    if (gSync.sendTabConfiguredAndLoading) {
      bodyNode.setAttribute("state""notready");
    }
    if (reloadDevices) {
      // Force a refresh of the fxa device list in case the user connected a new
      // device, and is waiting for it to show up.
      this.refreshFxaDevices().then(_ => {
        if (!window.closed) {
          this._populateSendTabToDevicesView(panelViewNode, false);
        }
      });
    }
  },

  async toggleAccountPanel(anchor = null, aEvent) {
    // Don't show the panel if the window is in customization mode.
    if (document.documentElement.hasAttribute("customizing")) {
      return;
    }

    if (
      (aEvent.type == "mousedown" && aEvent.button != 0) ||
      (aEvent.type == "keypress" &&
        aEvent.charCode != KeyEvent.DOM_VK_SPACE &&
        aEvent.keyCode != KeyEvent.DOM_VK_RETURN)
    ) {
      return;
    }

    const fxaToolbarMenuBtn = document.getElementById(
      "fxa-toolbar-menu-button"
    );

    if (anchor === null) {
      anchor = fxaToolbarMenuBtn;
    }

    if (anchor == fxaToolbarMenuBtn && anchor.getAttribute("open") != "true") {
      if (ASRouter.initialized) {
        await ASRouter.sendTriggerMessage({
          browser: gBrowser.selectedBrowser,
          id: "menuOpened",
          context: { source: MenuMessage.SOURCES.PXI_MENU },
        });
      }
    }

    // We read the state that's been set on the root node, since that makes
    // it easier to test the various front-end states without having to actually
    // have UIState know about it.
    let fxaStatus = document.documentElement.getAttribute("fxastatus");

    if (fxaStatus == "not_configured") {
      // sign in button in app (hamburger) menu
      // should take you straight to fxa sign in page
      if (anchor.id == "appMenu-fxa-label2") {
        this.openFxAEmailFirstPageFromFxaMenu(anchor);
        PanelUI.hide();
        return;
      }

      // If we're signed out but have the PXI pref enabled
      // we should show the PXI panel instead of taking the user
      // straight to FxA sign-in
      if (this.FXA_CTA_MENU_ENABLED) {
        this.updateFxAPanel(UIState.get());
        this.updateCTAPanel(anchor);
        PanelUI.showSubView("PanelUI-fxa", anchor, aEvent);
      } else if (anchor == fxaToolbarMenuBtn) {
        // The fxa toolbar button doesn't have much context before the user
        // clicks it so instead of going straight to the login page,
        // we take them to a page that has more information
        this.emitFxaToolbarTelemetry("toolbar_icon", anchor);
        openTrustedLinkIn("about:preferences#sync""tab");
        PanelUI.hide();
      }
      return;
    }
    // If the user is signed in and we have the PXI pref enabled then add
    // the pxi panel to the existing toolbar
    if (this.FXA_CTA_MENU_ENABLED) {
      this.updateCTAPanel(anchor);
    }

    if (!gFxaToolbarAccessed) {
      Services.prefs.setBoolPref("identity.fxaccounts.toolbar.accessed"true);
    }

    this.enableSendTabIfValidTab();

    if (!this.getSendTabTargets().length) {
      PanelMultiView.getViewNode(
        document,
        "PanelUI-fxa-menu-sendtab-button"
      ).hidden = true;
    }

    if (anchor.getAttribute("open") == "true") {
      PanelUI.hide();
    } else {
      this.emitFxaToolbarTelemetry("toolbar_icon", anchor);
      PanelUI.showSubView("PanelUI-fxa", anchor, aEvent);
    }
  },

  _disableSyncOffIndicator() {
    const newSyncSetupEnabled =
      NimbusFeatures.syncSetupFlow.getVariable("enabled");
    const SYNC_PANEL_ACCESSED_PREF =
      "identity.fxaccounts.toolbar.syncSetup.panelAccessed";
    // If the user was enrolled in the experiment and hasn't previously accessed
    // the panel, we disable the sync off indicator
    if (
      newSyncSetupEnabled &&
      !Services.prefs.getBoolPref(SYNC_PANEL_ACCESSED_PREF, false)
    ) {
      // Turn off the indicator so the user doesn't see it in subsequent openings
      Services.prefs.setBoolPref(SYNC_PANEL_ACCESSED_PREF, true);
    }
  },

  _shouldShowSyncOffIndicator() {
    // We only ever want to show the user the dot once, once they've clicked into the panel
    // we do not show them the dot anymore
    if (
      Services.prefs.getBoolPref(
        "identity.fxaccounts.toolbar.syncSetup.panelAccessed",
        false
      )
    ) {
      return false;
    }
    return NimbusFeatures.syncSetupFlow.getVariable("enabled");
  },

  updateFxAPanel(state = {}) {
    const isNewSyncSetupFlowEnabled =
      NimbusFeatures.syncSetupFlow.getVariable("enabled");
    const mainWindowEl = document.documentElement;

    const menuHeaderTitleEl = PanelMultiView.getViewNode(
      document,
      "fxa-menu-header-title"
    );
    const menuHeaderDescriptionEl = PanelMultiView.getViewNode(
      document,
      "fxa-menu-header-description"
    );
    const cadButtonEl = PanelMultiView.getViewNode(
      document,
      "PanelUI-fxa-menu-connect-device-button"
    );
    const syncSetupEl = PanelMultiView.getViewNode(
      document,
      isNewSyncSetupFlowEnabled
        ? "PanelUI-fxa-menu-setup-sync-container"
        : "PanelUI-fxa-menu-setup-sync-button"
    );
    const syncNowButtonEl = PanelMultiView.getViewNode(
      document,
      "PanelUI-fxa-menu-syncnow-button"
    );
    const fxaMenuAccountButtonEl = PanelMultiView.getViewNode(
      document,
      "fxa-manage-account-button"
    );
    const signedInContainer = PanelMultiView.getViewNode(
      document,
      "PanelUI-signedin-panel"
    );

    // Reset UI elements to default state
    cadButtonEl.setAttribute("disabled"true);
    cadButtonEl.hidden = isNewSyncSetupFlowEnabled;
    syncNowButtonEl.hidden = true;
    signedInContainer.hidden = true;
    fxaMenuAccountButtonEl.classList.remove("subviewbutton-nav");
    fxaMenuAccountButtonEl.removeAttribute("closemenu");
    syncSetupEl.removeAttribute("hidden");
    menuHeaderDescriptionEl.hidden = false;

    // The Firefox Account toolbar currently handles 3 different states for
    // users. The default `not_configured` state shows an empty avatar, `unverified`
    // state shows an avatar with an email icon, `login-failed` state shows an avatar
    // with a danger icon and the `verified` state will show the users
    // custom profile image or a filled avatar.
    let stateValue = "not_configured";
    let headerTitleL10nId;
    let headerDescription;

    switch (state.status) {
      case UIState.STATUS_NOT_CONFIGURED:
        mainWindowEl.style.removeProperty("--avatar-image-url");
        headerTitleL10nId = this.FXA_CTA_MENU_ENABLED
          ? "synced-tabs-fxa-sign-in"
          : "appmenuitem-sign-in-account";
        headerDescription = this.fluentStrings.formatValueSync(
          this.FXA_CTA_MENU_ENABLED
            ? "fxa-menu-sync-description"
            : "appmenu-fxa-signed-in-label"
        );
        if (this.FXA_CTA_MENU_ENABLED) {
          const ctaCopy = this.getMenuCtaCopy(NimbusFeatures.fxaAvatarMenuItem);
          if (ctaCopy) {
            headerTitleL10nId = ctaCopy.headerTitleL10nId;
            headerDescription = ctaCopy.headerDescription;
          }
        }
        break;

      case UIState.STATUS_LOGIN_FAILED:
        stateValue = "login-failed";
        headerTitleL10nId = "account-disconnected2";
        headerDescription = state.displayName || state.email;
        mainWindowEl.style.removeProperty("--avatar-image-url");
        break;

      case UIState.STATUS_NOT_VERIFIED:
        stateValue = "unverified";
        headerTitleL10nId = "account-finish-account-setup";
        headerDescription = state.displayName || state.email;
        break;

      case UIState.STATUS_SIGNED_IN:
        stateValue = "signedin";
        headerTitleL10nId = "appmenuitem-fxa-manage-account";
        headerDescription = state.displayName || state.email;
        this.updateAvatarURL(
          mainWindowEl,
          state.avatarURL,
          state.avatarIsDefault
        );
        signedInContainer.hidden = false;
        cadButtonEl.removeAttribute("disabled");

        if (state.syncEnabled) {
          syncNowButtonEl.removeAttribute("hidden");
          syncSetupEl.hidden = true;
        } else if (this._shouldShowSyncOffIndicator()) {
          let fxaButton = document.getElementById("fxa-toolbar-menu-button");
          fxaButton?.setAttribute("badge-status""sync-disabled");
        }
        break;

      default:
        headerTitleL10nId = this.FXA_CTA_MENU_ENABLED
          ? "synced-tabs-fxa-sign-in"
          : "appmenuitem-sign-in-account";
        headerDescription = this.fluentStrings.formatValueSync(
          "fxa-menu-turn-on-sync-default"
        );
        break;
    }

    // Update UI elements with determined values
    mainWindowEl.setAttribute("fxastatus", stateValue);
    menuHeaderTitleEl.value =
      this.fluentStrings.formatValueSync(headerTitleL10nId);
    // If we description is empty, we hide it
    menuHeaderDescriptionEl.hidden = !headerDescription;
    menuHeaderDescriptionEl.value = headerDescription;
    // We remove the data-l10n-id attribute here to prevent the node's value
    // attribute from being overwritten by Fluent when the panel is moved
    // around in the DOM.
    menuHeaderTitleEl.removeAttribute("data-l10n-id");
    menuHeaderDescriptionEl.removeAttribute("data-l10n-id");
  },

  updateAvatarURL(mainWindowEl, avatarURL, avatarIsDefault) {
    if (avatarURL && !avatarIsDefault) {
      const bgImage = `url("${avatarURL}")`;
      const img = new Image();
      img.onload = () => {
        mainWindowEl.style.setProperty("--avatar-image-url", bgImage);
      };
      img.onerror = () => {
        mainWindowEl.style.removeProperty("--avatar-image-url");
      };
      img.src = avatarURL;
    } else {
      mainWindowEl.style.removeProperty("--avatar-image-url");
    }
  },

  enableSendTabIfValidTab() {
    // All tabs selected must be sendable for the Send Tab button to be enabled
    // on the FxA menu.
    let canSendAllURIs = gBrowser.selectedTabs.every(
      t => !!BrowserUtils.getShareableURL(t.linkedBrowser.currentURI)
    );

    PanelMultiView.getViewNode(
      document,
      "PanelUI-fxa-menu-sendtab-button"
    ).hidden = !canSendAllURIs;
  },

  // This is mis-named - it can be used to record any FxA UI telemetry, whether from
  // the toolbar or not. The required `sourceElement` param is enough to help us know
  // how to record the interaction.
  emitFxaToolbarTelemetry(type, sourceElement) {
    if (UIState.isReady()) {
      const state = UIState.get();
      const hasAvatar = state.avatarURL && !state.avatarIsDefault;
      let extraOptions = {
        fxa_status: state.status,
        fxa_avatar: hasAvatar ? "true" : "false",
        fxa_sync_on: state.syncEnabled,
      };

      let eventName = this._getEntryPointForElement(sourceElement);
      let category = "";
      if (eventName == "fxa_avatar_menu") {
        category = "fxaAvatarMenu";
      } else if (eventName == "fxa_app_menu") {
        category = "fxaAppMenu";
      } else {
        return;
      }
      Glean[category][
        "click" +
          type
            .split("_")
            .map(word => word[0].toUpperCase() + word.slice(1))
            .join("")
      ]?.record(extraOptions);
    }
  },

  updatePanelPopup({ email, displayName, status }) {
    const appMenuStatus = PanelMultiView.getViewNode(
      document,
      "appMenu-fxa-status2"
    );
    const appMenuLabel = PanelMultiView.getViewNode(
      document,
      "appMenu-fxa-label2"
    );
    const appMenuHeaderText = PanelMultiView.getViewNode(
      document,
      "appMenu-fxa-text"
    );
    const appMenuHeaderTitle = PanelMultiView.getViewNode(
      document,
      "appMenu-header-title"
    );
    const appMenuHeaderDescription = PanelMultiView.getViewNode(
      document,
      "appMenu-header-description"
    );
    const fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa");

    let defaultLabel = this.fluentStrings.formatValueSync(
      "appmenu-fxa-signed-in-label"
    );
    // Reset the status bar to its original state.
    appMenuLabel.setAttribute("label", defaultLabel);
    appMenuLabel.removeAttribute("aria-labelledby");
    appMenuStatus.removeAttribute("fxastatus");

    if (status == UIState.STATUS_NOT_CONFIGURED) {
      appMenuHeaderText.hidden = false;
      appMenuStatus.classList.add("toolbaritem-combined-buttons");
      appMenuLabel.classList.remove("subviewbutton-nav");
      appMenuHeaderTitle.hidden = true;
      appMenuHeaderDescription.value = defaultLabel;
      return;
    }
    appMenuLabel.classList.remove("subviewbutton-nav");

    appMenuHeaderText.hidden = true;
    appMenuStatus.classList.remove("toolbaritem-combined-buttons");

    // While we prefer the display name in most case, in some strings
    // where the context is something like "Verify %s", the email
    // is used even when there's a display name.
    if (status == UIState.STATUS_LOGIN_FAILED) {
      const [tooltipDescription, errorLabel] =
        this.fluentStrings.formatValuesSync([
          { id: "account-reconnect", args: { email } },
          { id: "account-disconnected2" },
        ]);
      appMenuStatus.setAttribute("fxastatus""login-failed");
      appMenuStatus.setAttribute("tooltiptext", tooltipDescription);
      appMenuLabel.classList.add("subviewbutton-nav");
      appMenuHeaderTitle.hidden = false;
      appMenuHeaderTitle.value = errorLabel;
      appMenuHeaderDescription.value = displayName || email;

      appMenuLabel.removeAttribute("label");
      appMenuLabel.setAttribute(
        "aria-labelledby",
        `${appMenuHeaderTitle.id},${appMenuHeaderDescription.id}`
      );
      return;
    } else if (status == UIState.STATUS_NOT_VERIFIED) {
      const [tooltipDescription, unverifiedLabel] =
        this.fluentStrings.formatValuesSync([
          { id: "account-verify", args: { email } },
          { id: "account-finish-account-setup" },
        ]);
      appMenuStatus.setAttribute("fxastatus""unverified");
      appMenuStatus.setAttribute("tooltiptext", tooltipDescription);
      appMenuLabel.classList.add("subviewbutton-nav");
      appMenuHeaderTitle.hidden = false;
      appMenuHeaderTitle.value = unverifiedLabel;
      appMenuHeaderDescription.value = email;

      appMenuLabel.removeAttribute("label");
      appMenuLabel.setAttribute(
        "aria-labelledby",
        `${appMenuHeaderTitle.id},${appMenuHeaderDescription.id}`
      );
      return;
    }

    appMenuHeaderTitle.hidden = true;
    appMenuHeaderDescription.value = displayName || email;
    appMenuStatus.setAttribute("fxastatus""signedin");
    appMenuLabel.setAttribute("label", displayName || email);
    appMenuLabel.classList.add("subviewbutton-nav");
    fxaPanelView.setAttribute(
      "title",
      this.fluentStrings.formatValueSync("appmenu-account-header")
    );
    appMenuStatus.removeAttribute("tooltiptext");
  },

  updateState(state) {
    for (let [shown, menuId, boxId] of [
      [
        state.status == UIState.STATUS_NOT_CONFIGURED,
        "sync-setup",
        "PanelUI-remotetabs-setupsync",
      ],
      [
        state.status == UIState.STATUS_SIGNED_IN && !state.syncEnabled,
        "sync-enable",
        "PanelUI-remotetabs-syncdisabled",
      ],
      [
        state.status == UIState.STATUS_LOGIN_FAILED,
        "sync-reauthitem",
        "PanelUI-remotetabs-reauthsync",
      ],
      [
        state.status == UIState.STATUS_NOT_VERIFIED,
        "sync-unverifieditem",
        "PanelUI-remotetabs-unverified",
      ],
      [
        state.status == UIState.STATUS_SIGNED_IN && state.syncEnabled,
        "sync-syncnowitem",
        "PanelUI-remotetabs-main",
      ],
    ]) {
      document.getElementById(menuId).hidden = PanelMultiView.getViewNode(
        document,
        boxId
      ).hidden = !shown;
    }
  },

  updateSyncStatus(state) {
    let syncNow =
      document.querySelector(".syncNowBtn") ||
      document
        .getElementById("appMenu-viewCache")
        .content.querySelector(".syncNowBtn");
    const syncingUI = syncNow.getAttribute("syncstatus") == "active";
    if (state.syncing != syncingUI) {
      // Do we need to update the UI?
      state.syncing ? this.onActivityStart() : this.onActivityStop();
    }
  },

  async openSignInAgainPage(entryPoint) {
    if (!(await FxAccounts.canConnectAccount())) {
      return;
    }
    const url = await FxAccounts.config.promiseConnectAccountURI(entryPoint);
    switchToTabHavingURI(url, true, {
      replaceQueryString: true,
      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    });
  },

  async openDevicesManagementPage(entryPoint) {
    let url = await FxAccounts.config.promiseManageDevicesURI(entryPoint);
    switchToTabHavingURI(url, true, {
      replaceQueryString: true,
      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
    });
  },

  async openConnectAnotherDevice(entryPoint) {
    const url = await FxAccounts.config.promiseConnectDeviceURI(entryPoint);
    openTrustedLinkIn(url, "tab");
  },

  async clickOpenConnectAnotherDevice(sourceElement) {
    this.emitFxaToolbarTelemetry("cad", sourceElement);
    let entryPoint = this._getEntryPointForElement(sourceElement);
    this.openConnectAnotherDevice(entryPoint);
  },

  openSendToDevicePromo() {
    const url = Services.urlFormatter.formatURLPref(
      "identity.sendtabpromo.url"
    );
    switchToTabHavingURI(url, true, { replaceQueryString: true });
  },

  async clickFxAMenuHeaderButton(sourceElement) {
    // Depending on the current logged in state of a user,
    // clicking the FxA header will either open
    // a sign-in page, account management page, or sync
    // preferences page.
    const { status } = UIState.get();
    switch (status) {
      case UIState.STATUS_NOT_CONFIGURED:
        this.openFxAEmailFirstPageFromFxaMenu(sourceElement);
        break;
      case UIState.STATUS_LOGIN_FAILED:
        this.openPrefsFromFxaMenu("sync_settings", sourceElement);
        break;
      case UIState.STATUS_NOT_VERIFIED:
        this.openFxAEmailFirstPage("fxa_app_menu_reverify");
        break;
      case UIState.STATUS_SIGNED_IN:
        this._openFxAManagePageFromElement(sourceElement);
    }
  },

  // Gets the telemetry "entry point" we should use for a given UI element.
  // This entry-point is recorded in both client telemetry (typically called the "object")
  // and where applicable, also communicated to the server for server telemetry via a URL query param.
  //
  // It inspects the parent elements to determine if the element is within one of our "well known"
  // UI groups, in which case it will return a string for that group (eg, "fxa_app_menu", "fxa_toolbar_button").
  // Otherwise (eg, the item might be directly on the context menu), it will return "fxa_discoverability_native".
  _getEntryPointForElement(sourceElement) {
    // Note that when an element is in either the app menu or the toolbar button menu,
    // in both cases it *will* have a parent with ID "PanelUI-fxa-menu". But when
    // in the app menu, it will also have a grand-parent with ID "appMenu-popup".
    // So we must check for that outer grandparent first.
    const appMenuPanel = document.getElementById("appMenu-popup");
    if (appMenuPanel.contains(sourceElement)) {
      return "fxa_app_menu";
    }
    // If it *is* the toolbar button...
    if (sourceElement.id == "fxa-toolbar-menu-button") {
      return "fxa_avatar_menu";
    }
    // ... or is in the panel shown by that button.
    const fxaMenu = document.getElementById("PanelUI-fxa-menu");
    if (fxaMenu && fxaMenu.contains(sourceElement)) {
      return "fxa_avatar_menu";
    }
    return "fxa_discoverability_native";
  },

  async openFxAEmailFirstPage(entryPoint, extraParams = {}) {
    if (!(await FxAccounts.canConnectAccount())) {
      return;
    }
    const url = await FxAccounts.config.promiseConnectAccountURI(
      entryPoint,
      extraParams
    );
    switchToTabHavingURI(url, true, { replaceQueryString: true });
  },

  async openFxAEmailFirstPageFromFxaMenu(sourceElement, extraParams = {}) {
    this.emitFxaToolbarTelemetry("login", sourceElement);
    this.openFxAEmailFirstPage(
      this._getEntryPointForElement(sourceElement),
      extraParams
    );
  },

  async openFxAManagePage(entryPoint) {
    const url = await FxAccounts.config.promiseManageURI(entryPoint);
    switchToTabHavingURI(url, true, { replaceQueryString: true });
  },

  async _openFxAManagePageFromElement(sourceElement) {
    this.emitFxaToolbarTelemetry("account_settings", sourceElement);
    this.openFxAManagePage(this._getEntryPointForElement(sourceElement));
  },

  // Returns true if we managed to send the tab to any targets, false otherwise.
  async sendTabToDevice(url, targets, title) {
    const fxaCommandsDevices = [];
    for (const target of targets) {
      if (fxAccounts.commands.sendTab.isDeviceCompatible(target)) {
        fxaCommandsDevices.push(target);
      } else {
        this.log.error(`Target ${target.id} unsuitable for send tab.`);
      }
    }
    // If a primary-password is enabled then it must be unlocked so FxA can get
    // the encryption keys from the login manager. (If we end up using the "sync"
    // fallback that would end up prompting by itself, but the FxA command route
    // will not) - so force that here.
    let cryptoSDR = Cc["@mozilla.org/login-manager/crypto/SDR;1"].getService(
      Ci.nsILoginManagerCrypto
    );
    if (!cryptoSDR.isLoggedIn) {
      if (cryptoSDR.uiBusy) {
        this.log.info("Master password UI is busy - not sending the tabs");
        return false;
      }
      try {
        cryptoSDR.encrypt("bacon"); // forces the mp prompt.
      } catch (e) {
        this.log.info(
          "Master password remains unlocked - not sending the tabs"
        );
        return false;
      }
    }
    let numFailed = 0;
    if (fxaCommandsDevices.length) {
      this.log.info(
        `Sending a tab to ${fxaCommandsDevices
          .map(d => d.id)
          .join(", ")} using FxA commands.`
      );
      const report = await fxAccounts.commands.sendTab.send(
        fxaCommandsDevices,
        { url, title }
      );
      for (let { device, error } of report.failed) {
        this.log.error(
          `Failed to send a tab with FxA commands for ${device.id}.`,
          error
        );
        numFailed++;
      }
    }
    return numFailed < targets.length; // Good enough.
  },

  populateSendTabToDevicesMenu(
    devicesPopup,
    uri,
    title,
    multiselected,
    createDeviceNodeFn,
    isFxaMenu = false
  ) {
    uri = BrowserUtils.getShareableURL(uri);
    if (!uri) {
      // log an error as everyone should have already checked this.
      this.log.error("Ignoring request to share a non-sharable URL");
      return;
    }
    if (!createDeviceNodeFn) {
      createDeviceNodeFn = (targetId, name) => {
        let eltName = name ? "menuitem" : "menuseparator";
        return document.createXULElement(eltName);
      };
    }

    // remove existing menu items
    for (let i = devicesPopup.children.length - 1; i >= 0; --i) {
      let child = devicesPopup.children[i];
      if (child.classList.contains("sync-menuitem")) {
        child.remove();
      }
    }

    if (gSync.sendTabConfiguredAndLoading) {
      // We can only be in this case in the page action menu.
      return;
    }

    const fragment = document.createDocumentFragment();

    const state = UIState.get();
    if (state.status == UIState.STATUS_SIGNED_IN) {
      const targets = this.getSendTabTargets();
      if (targets.length) {
        this._appendSendTabDeviceList(
          targets,
          fragment,
          createDeviceNodeFn,
          uri.spec,
          title,
          multiselected,
          isFxaMenu
        );
      } else {
        this._appendSendTabSingleDevice(fragment, createDeviceNodeFn);
      }
    } else if (
      state.status == UIState.STATUS_NOT_VERIFIED ||
      state.status == UIState.STATUS_LOGIN_FAILED
    ) {
      this._appendSendTabVerify(fragment, createDeviceNodeFn);
    } else {
      // The only status not handled yet is STATUS_NOT_CONFIGURED, and
      // when we're in that state, none of the menus that call
      // populateSendTabToDevicesMenu are available, so entering this
      // state is unexpected.
      throw new Error(
        "Called populateSendTabToDevicesMenu when in STATUS_NOT_CONFIGURED " +
          "state."
      );
    }

    devicesPopup.appendChild(fragment);
  },

  _appendSendTabDeviceList(
    targets,
    fragment,
    createDeviceNodeFn,
    url,
    title,
    multiselected,
    isFxaMenu = false
  ) {
    let tabsToSend = multiselected
      ? gBrowser.selectedTabs.map(t => {
          return {
            url: t.linkedBrowser.currentURI.spec,
            title: t.linkedBrowser.contentTitle,
          };
        })
      : [{ url, title }];

    const send = to => {
      Promise.all(
        tabsToSend.map(t =>
          // sendTabToDevice does not reject.
          this.sendTabToDevice(t.url, to, t.title)
        )
      ).then(results => {
        // Show the Sent! confirmation if any of the sends succeeded.
        if (results.includes(true)) {
          // FxA button could be hidden with CSS since the user is logged out,
          // although it seems likely this would only happen in testing...
          let fxastatus = document.documentElement.getAttribute("fxastatus");
          let anchorNode =
            (fxastatus &&
              fxastatus != "not_configured" &&
              document.getElementById("fxa-toolbar-menu-button")?.parentNode
                ?.id != "widget-overflow-list" &&
              document.getElementById("fxa-toolbar-menu-button")) ||
            document.getElementById("PanelUI-menu-button");
          ConfirmationHint.show(anchorNode, "confirmation-hint-send-to-device");
        }
        fxAccounts.flushLogFile();
      });
    };
    const onSendAllCommand = () => {
      send(targets);
    };
    const onTargetDeviceCommand = event => {
      const targetId = event.target.getAttribute("clientId");
      const target = targets.find(t => t.id == targetId);
      send([target]);
    };

    function addTargetDevice(targetId, name, targetType, lastModified) {
      const targetDevice = createDeviceNodeFn(
        targetId,
        name,
        targetType,
        lastModified
      );
      targetDevice.addEventListener(
        "command",
        targetId ? onTargetDeviceCommand : onSendAllCommand,
        true
      );
      targetDevice.classList.add("sync-menuitem""sendtab-target");
      targetDevice.setAttribute("clientId", targetId);
      targetDevice.setAttribute("clientType", targetType);
      targetDevice.setAttribute("label", name);
      fragment.appendChild(targetDevice);
    }

    for (let target of targets) {
      let type, lastModified;
      if (target.clientRecord) {
        type = Weave.Service.clientsEngine.getClientType(
          target.clientRecord.id
        );
        lastModified = new Date(target.clientRecord.serverLastModified * 1000);
      } else {
        // For phones, FxA uses "mobile" and Sync clients uses "phone".
        type = target.type == "mobile" ? "phone" : target.type;
        lastModified = target.lastAccessTime
          ? new Date(target.lastAccessTime)
          : null;
      }
      addTargetDevice(target.id, target.name, type, lastModified);
    }

    if (targets.length > 1) {
      // "Send to All Devices" menu item
      const separator = createDeviceNodeFn();
      separator.classList.add("sync-menuitem");
      fragment.appendChild(separator);
      const [allDevicesLabel, manageDevicesLabel] =
        this.fluentStrings.formatValuesSync(
          isFxaMenu
            ? ["account-send-to-all-devices""account-manage-devices"]
            : [
                "account-send-to-all-devices-titlecase",
                "account-manage-devices-titlecase",
              ]
        );
      addTargetDevice("", allDevicesLabel, "");

      // "Manage devices" menu item
      // We piggyback on the createDeviceNodeFn implementation,
      // it's a big disgusting.
      const targetDevice = createDeviceNodeFn(
        null,
        manageDevicesLabel,
        null,
        null
      );
      targetDevice.addEventListener(
        "command",
        () => gSync.openDevicesManagementPage("sendtab"),
        true
      );
      targetDevice.classList.add("sync-menuitem""sendtab-target");
      targetDevice.setAttribute("label", manageDevicesLabel);
      fragment.appendChild(targetDevice);
    }
  },

  _appendSendTabSingleDevice(fragment, createDeviceNodeFn) {
    const [noDevices, learnMore, connectDevice] =
      this.fluentStrings.formatValuesSync([
        "account-send-tab-to-device-singledevice-status",
        "account-send-tab-to-device-singledevice-learnmore",
        "account-send-tab-to-device-connectdevice",
      ]);
    const actions = [
      {
        label: connectDevice,
        command: () => this.openConnectAnotherDevice("sendtab"),
      },
      { label: learnMore, command: () => this.openSendToDevicePromo() },
    ];
    this._appendSendTabInfoItems(
      fragment,
      createDeviceNodeFn,
      noDevices,
      actions
    );
  },

  _appendSendTabVerify(fragment, createDeviceNodeFn) {
    const [notVerified, verifyAccount] = this.fluentStrings.formatValuesSync([
      "account-send-tab-to-device-verify-status",
      "account-send-tab-to-device-verify",
    ]);
    const actions = [
      { label: verifyAccount, command: () => this.openPrefs("sendtab") },
    ];
    this._appendSendTabInfoItems(
      fragment,
      createDeviceNodeFn,
      notVerified,
      actions
    );
  },

  _appendSendTabInfoItems(fragment, createDeviceNodeFn, statusLabel, actions) {
    const status = createDeviceNodeFn(null, statusLabel, null);
    status.setAttribute("label", statusLabel);
    status.setAttribute("disabled"true);
    status.classList.add("sync-menuitem");
    fragment.appendChild(status);

    const separator = createDeviceNodeFn(nullnullnull);
    separator.classList.add("sync-menuitem");
    fragment.appendChild(separator);

    for (let { label, command } of actions) {
      const actionItem = createDeviceNodeFn(null, label, null);
      actionItem.addEventListener("command", command, true);
      actionItem.classList.add("sync-menuitem");
      actionItem.setAttribute("label", label);
      fragment.appendChild(actionItem);
    }
  },

  // "Send Tab to Device" menu item
  updateTabContextMenu(aPopupMenu, aTargetTab) {
    // We may get here before initialisation. This situation
    // can lead to a empty label for 'Send To Device' Menu.
    this.init();

    if (!this.FXA_ENABLED) {
      // These items are hidden in onFxaDisabled(). No need to do anything.
      return;
    }
    let hasASendableURI = false;
    for (let tab of aTargetTab.multiselected
      ? gBrowser.selectedTabs
      : [aTargetTab]) {
      if (BrowserUtils.getShareableURL(tab.linkedBrowser.currentURI)) {
        hasASendableURI = true;
        break;
      }
    }
    const enabled = !this.sendTabConfiguredAndLoading && hasASendableURI;
    const hideItems = this.shouldHideSendContextMenuItems(enabled);

    let sendTabsToDevice = document.getElementById("context_sendTabToDevice");
    sendTabsToDevice.disabled = !enabled;

    if (hideItems || !hasASendableURI) {
      sendTabsToDevice.hidden = true;
    } else {
      let tabCount = aTargetTab.multiselected
        ? gBrowser.multiSelectedTabsCount
        : 1;
      sendTabsToDevice.setAttribute(
        "data-l10n-args",
        JSON.stringify({ tabCount })
      );
      sendTabsToDevice.hidden = false;
    }
  },

  // "Send Page to Device" and "Send Link to Device" menu items
  updateContentContextMenu(contextMenu) {
    if (!this.FXA_ENABLED) {
--> --------------------

--> maximum size reached

--> --------------------

88%


[ Verzeichnis aufwärts0.31unsichere Verbindung  Übersetzung europäischer Sprachen durch Browser  ]