summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-sync.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/browser-sync.js')
-rw-r--r--browser/base/content/browser-sync.js1609
1 files changed, 1609 insertions, 0 deletions
diff --git a/browser/base/content/browser-sync.js b/browser/base/content/browser-sync.js
new file mode 100644
index 0000000000..3f5879fea8
--- /dev/null
+++ b/browser/base/content/browser-sync.js
@@ -0,0 +1,1609 @@
+/* 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 { UIState } = ChromeUtils.import("resource://services-sync/UIState.jsm");
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "FxAccounts",
+ "resource://gre/modules/FxAccounts.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "EnsureFxAccountsWebChannel",
+ "resource://gre/modules/FxAccountsWebChannel.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "Weave",
+ "resource://services-sync/main.js"
+);
+
+const MIN_STATUS_ANIMATION_DURATION = 1600;
+
+var gSync = {
+ _initialized: 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.import("resource://gre/modules/Log.jsm");
+ let syncLog = Log.repository.getLogger("Sync.Browser");
+ syncLog.manageLevelFromPref("services.sync.log.logger.browser");
+ this._log = syncLog;
+ }
+ return this._log;
+ },
+
+ get fxaStrings() {
+ delete this.fxaStrings;
+ return (this.fxaStrings = Services.strings.createBundle(
+ "chrome://browser/locale/accounts.properties"
+ ));
+ },
+
+ get syncStrings() {
+ delete this.syncStrings;
+ // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
+ // but for now just make it work
+ return (this.syncStrings = Services.strings.createBundle(
+ "chrome://weave/locale/sync.properties"
+ ));
+ },
+
+ // 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;
+ },
+
+ getSendTabTargets() {
+ // If sync is not enabled, then there's no point looking for sync clients.
+ // If sync is simply not ready or hasn't yet synced the clients engine, we
+ // just assume the fxa device doesn't have a sync record - in practice,
+ // that just means we don't attempt to fall back to the "old" sendtab should
+ // "new" sendtab fail.
+ // We should just kill "old" sendtab now all our mobile browsers support
+ // "new".
+ let getClientRecord = () => undefined;
+ if (UIState.get().syncEnabled && Weave.Service.clientsEngine) {
+ getClientRecord = id =>
+ Weave.Service.clientsEngine.getClientByFxaDeviceId(id);
+ }
+ let targets = [];
+ if (!fxAccounts.device.recentDeviceList) {
+ return targets;
+ }
+ for (let d of fxAccounts.device.recentDeviceList) {
+ if (d.isCurrentDevice) {
+ continue;
+ }
+
+ let clientRecord = getClientRecord(d.id);
+ if (clientRecord || fxAccounts.commands.sendTab.isDeviceCompatible(d)) {
+ targets.push({
+ clientRecord,
+ ...d,
+ });
+ }
+ }
+ return targets.sort((a, b) => a.name.localeCompare(b.name));
+ },
+
+ _generateNodeGetters() {
+ for (let k of ["Status", "Avatar", "Label"]) {
+ let prop = "appMenu" + k;
+ let suffix = k.toLowerCase();
+ delete this[prop];
+ this.__defineGetter__(prop, function() {
+ delete this[prop];
+ return (this[prop] = document.getElementById("appMenu-fxa-" + suffix));
+ });
+ }
+ },
+
+ _definePrefGetters() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "UNSENDABLE_URL_REGEXP",
+ "services.sync.engine.tabs.filteredUrls",
+ null,
+ null,
+ rx => {
+ try {
+ return new RegExp(rx, "i");
+ } catch (e) {
+ Cu.reportError(
+ `Failed to build url filter regexp for send tab: ${e}`
+ );
+ return null;
+ }
+ }
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "FXA_ENABLED",
+ "identity.fxaccounts.enabled"
+ );
+ },
+
+ 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");
+
+ this._generateNodeGetters();
+
+ // Label for the sync buttons.
+ const appMenuLabel = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-label"
+ );
+ 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;
+
+ for (let topic of this._obs) {
+ Services.obs.addObserver(this, topic, true);
+ }
+
+ this.maybeUpdateUIState();
+
+ EnsureFxAccountsWebChannel();
+
+ this._initialized = true;
+ },
+
+ uninit() {
+ if (!this._initialized) {
+ return;
+ }
+
+ for (let topic of this._obs) {
+ Services.obs.removeObserver(this, topic);
+ }
+
+ this._initialized = false;
+ },
+
+ observe(subject, topic, data) {
+ if (!this._initialized) {
+ Cu.reportError("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);
+ // Ensure we have something in the device list in the background.
+ this.ensureFxaDevices();
+ },
+
+ // 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(options) {
+ 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;
+ }
+ },
+
+ updateSendToDeviceTitle() {
+ let string = gBrowserBundle.GetStringFromName("sendTabsToDevice.label");
+ let title = PluralForm.get(1, string).replace("#1", 1);
+ if (gBrowser.selectedTab.multiselected) {
+ let tabCount = gBrowser.selectedTabs.length;
+ title = PluralForm.get(tabCount, string).replace("#1", tabCount);
+ }
+
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sendtab-button"
+ ).setAttribute("label", title);
+ },
+
+ 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);
+ },
+
+ showRemoteTabsFromFxaMenu(panel) {
+ PanelUI.showSubView("PanelUI-remotetabs", panel);
+ this.emitFxaToolbarTelemetry("sync_tabs", panel);
+ },
+
+ showSidebarFromFxaMenu(panel) {
+ SidebarUI.toggle("viewTabsSidebar");
+ this.emitFxaToolbarTelemetry("sync_tabs_sidebar", panel);
+ },
+
+ populateSendTabToDevicesView(panelViewNode, reloadDevices = true) {
+ let bodyNode = panelViewNode.querySelector(".panel-subview-body");
+ let panelNode = panelViewNode.closest("panel");
+ let browser = gBrowser.selectedBrowser;
+ let url = browser.currentURI.spec;
+ 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,
+ url,
+ title,
+ multiselected,
+ (clientId, name, clientType, lastModified) => {
+ if (!name) {
+ return document.createXULElement("toolbarseparator");
+ }
+ let item = document.createXULElement("toolbarbutton");
+ item.classList.add("pageAction-sendToDevice-device", "subviewbutton");
+ if (clientId) {
+ item.classList.add("subviewbutton-iconic");
+ if (lastModified) {
+ item.setAttribute(
+ "tooltiptext",
+ gSync.formatLastSyncDate(lastModified)
+ );
+ }
+ }
+
+ item.addEventListener("command", event => {
+ if (panelNode) {
+ PanelMultiView.hidePopup(panelNode);
+ }
+ });
+ return item;
+ }
+ );
+
+ 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) {
+ // We will only pick up new Fennec clients if we sync the clients engine,
+ // but all other send-tab targets can be identified purely from the fxa
+ // device list. Syncing the clients engine doesn't force a refresh of the
+ // fxa list, and it seems overkill to force *both* a clients engine sync
+ // and an fxa device list refresh, especially given (a) the clients engine
+ // will sync by itself every 10 minutes and (b) Fennec is (at time of
+ // writing) about to be replaced by Fenix.
+ // So we suck up the fact that new Fennec clients may not appear for 10
+ // minutes and don't bother syncing the clients engine.
+
+ // 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);
+ }
+ });
+ }
+ },
+
+ toggleAccountPanel(
+ viewId,
+ anchor = document.getElementById("fxa-toolbar-menu-button"),
+ 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;
+ }
+
+ if (!gFxaToolbarAccessed) {
+ Services.prefs.setBoolPref("identity.fxaccounts.toolbar.accessed", true);
+ }
+
+ this.enableSendTabIfValidTab();
+
+ if (anchor.getAttribute("open") == "true") {
+ PanelUI.hide();
+ } else {
+ this.emitFxaToolbarTelemetry("toolbar_icon", anchor);
+ PanelUI.showSubView(viewId, anchor, aEvent);
+ }
+ },
+
+ updateFxAPanel(state = {}) {
+ const mainWindowEl = document.documentElement;
+
+ // 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";
+
+ 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 syncSetupButtonEl = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-setup-sync-button"
+ );
+
+ const syncPrefsButtonEl = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sync-prefs-button"
+ );
+
+ const syncNowButtonEl = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-syncnow-button"
+ );
+ const fxaMenuPanel = PanelMultiView.getViewNode(document, "PanelUI-fxa");
+
+ const fxaMenuAccountButtonEl = PanelMultiView.getViewNode(
+ document,
+ "fxa-manage-account-button"
+ );
+
+ let headerTitle = menuHeaderTitleEl.getAttribute("defaultLabel");
+ let headerDescription = menuHeaderDescriptionEl.getAttribute(
+ "defaultLabel"
+ );
+
+ const appMenuFxAButtonEl = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-label"
+ );
+
+ let panelTitle = this.fxaStrings.GetStringFromName("account.title");
+
+ fxaMenuPanel.removeAttribute("title");
+ cadButtonEl.setAttribute("disabled", true);
+ syncNowButtonEl.setAttribute("hidden", true);
+ fxaMenuAccountButtonEl.classList.remove("subviewbutton-nav");
+ fxaMenuAccountButtonEl.removeAttribute("closemenu");
+ syncPrefsButtonEl.setAttribute("hidden", true);
+ syncSetupButtonEl.removeAttribute("hidden");
+
+ if (state.status === UIState.STATUS_NOT_CONFIGURED) {
+ mainWindowEl.style.removeProperty("--avatar-image-url");
+ } else if (state.status === UIState.STATUS_LOGIN_FAILED) {
+ stateValue = "login-failed";
+ headerTitle = this.fxaStrings.GetStringFromName("account.reconnectToFxA");
+ headerDescription = state.email;
+ mainWindowEl.style.removeProperty("--avatar-image-url");
+ } else if (state.status === UIState.STATUS_NOT_VERIFIED) {
+ stateValue = "unverified";
+ headerTitle = this.fxaStrings.GetStringFromName(
+ "account.finishAccountSetup"
+ );
+ headerDescription = state.email;
+ } else if (state.status === UIState.STATUS_SIGNED_IN) {
+ stateValue = "signedin";
+ if (state.avatarURL && !state.avatarIsDefault) {
+ // The user has specified a custom avatar, attempt to load the image on all the menu buttons.
+ const bgImage = `url("${state.avatarURL}")`;
+ let img = new Image();
+ img.onload = () => {
+ // If the image has successfully loaded, update the menu buttons else
+ // we will use the default avatar image.
+ mainWindowEl.style.setProperty("--avatar-image-url", bgImage);
+ };
+ img.onerror = () => {
+ // If the image failed to load, remove the property and default
+ // to standard avatar.
+ mainWindowEl.style.removeProperty("--avatar-image-url");
+ };
+ img.src = state.avatarURL;
+ } else {
+ mainWindowEl.style.removeProperty("--avatar-image-url");
+ }
+
+ cadButtonEl.removeAttribute("disabled");
+
+ if (state.syncEnabled) {
+ syncNowButtonEl.removeAttribute("hidden");
+ syncPrefsButtonEl.removeAttribute("hidden");
+ syncSetupButtonEl.setAttribute("hidden", true);
+ }
+
+ fxaMenuAccountButtonEl.classList.add("subviewbutton-nav");
+ fxaMenuAccountButtonEl.setAttribute("closemenu", "none");
+
+ headerTitle = state.email;
+ headerDescription = this.fxaStrings.GetStringFromName(
+ "account.accountSettings"
+ );
+
+ panelTitle = state.displayName ? state.displayName : panelTitle;
+ }
+ mainWindowEl.setAttribute("fxastatus", stateValue);
+
+ menuHeaderTitleEl.value = headerTitle;
+ menuHeaderDescriptionEl.value = headerDescription;
+ appMenuFxAButtonEl.setAttribute("label", headerTitle);
+
+ fxaMenuPanel.setAttribute("title", panelTitle);
+ },
+
+ 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 =>
+ this.isSendableURI(t.linkedBrowser.currentURI.spec)
+ );
+
+ if (canSendAllURIs) {
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sendtab-button"
+ ).removeAttribute("disabled");
+ } else {
+ PanelMultiView.getViewNode(
+ document,
+ "PanelUI-fxa-menu-sendtab-button"
+ ).setAttribute("disabled", true);
+ }
+ },
+
+ emitFxaToolbarTelemetry(type, panel) {
+ if (UIState.isReady() && panel) {
+ const state = UIState.get();
+ const hasAvatar = state.avatarURL && !state.avatarIsDefault;
+ let extraOptions = {
+ fxa_status: state.status,
+ fxa_avatar: hasAvatar ? "true" : "false",
+ };
+
+ // When the fxa avatar panel is within the Firefox app menu,
+ // we emit different telemetry.
+ let eventName = "fxa_avatar_menu";
+ if (this.isPanelInsideAppMenu(panel)) {
+ eventName = "fxa_app_menu";
+ }
+
+ Services.telemetry.recordEvent(
+ eventName,
+ "click",
+ type,
+ null,
+ extraOptions
+ );
+ }
+ },
+
+ isPanelInsideAppMenu(panel = undefined) {
+ const appMenuPanel = document.getElementById("appMenu-popup");
+ if (panel && appMenuPanel.contains(panel)) {
+ return true;
+ }
+ return false;
+ },
+
+ updatePanelPopup(state) {
+ const appMenuStatus = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-status"
+ );
+ const appMenuLabel = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-label"
+ );
+ const appMenuAvatar = PanelMultiView.getViewNode(
+ document,
+ "appMenu-fxa-avatar"
+ );
+
+ let defaultLabel = appMenuStatus.getAttribute("defaultlabel");
+ const status = state.status;
+ // Reset the status bar to its original state.
+ appMenuLabel.setAttribute("label", defaultLabel);
+ appMenuStatus.removeAttribute("fxastatus");
+ appMenuAvatar.style.removeProperty("list-style-image");
+ appMenuLabel.classList.remove("subviewbutton-nav");
+
+ if (status == UIState.STATUS_NOT_CONFIGURED) {
+ return;
+ }
+
+ // At this point we consider sync to be configured (but still can be in an error state).
+ if (status == UIState.STATUS_LOGIN_FAILED) {
+ let tooltipDescription = this.fxaStrings.formatStringFromName(
+ "reconnectDescription",
+ [state.email]
+ );
+ let errorLabel = appMenuStatus.getAttribute("errorlabel");
+ appMenuStatus.setAttribute("fxastatus", "login-failed");
+ appMenuLabel.setAttribute("label", errorLabel);
+ appMenuStatus.setAttribute("tooltiptext", tooltipDescription);
+ return;
+ } else if (status == UIState.STATUS_NOT_VERIFIED) {
+ let tooltipDescription = this.fxaStrings.formatStringFromName(
+ "verifyDescription",
+ [state.email]
+ );
+ let unverifiedLabel = appMenuStatus.getAttribute("unverifiedlabel");
+ appMenuStatus.setAttribute("fxastatus", "unverified");
+ appMenuLabel.setAttribute("label", unverifiedLabel);
+ appMenuStatus.setAttribute("tooltiptext", tooltipDescription);
+ return;
+ }
+
+ // At this point we consider sync to be logged-in.
+ appMenuStatus.setAttribute("fxastatus", "signedin");
+ appMenuLabel.setAttribute("label", state.displayName || state.email);
+ appMenuLabel.classList.add("subviewbutton-nav");
+ 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) {
+ const url = await FxAccounts.config.promiseForceSigninURI(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 openConnectAnotherDeviceFromFxaMenu(panel = undefined) {
+ this.emitFxaToolbarTelemetry("cad", panel);
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+ this.openConnectAnotherDevice(entryPoint);
+ },
+
+ openSendToDevicePromo() {
+ const url = Services.urlFormatter.formatURLPref(
+ "identity.sendtabpromo.url"
+ );
+ switchToTabHavingURI(url, true, { replaceQueryString: true });
+ },
+
+ async clickFxAMenuHeaderButton(panel = undefined) {
+ // 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(panel);
+ break;
+ case UIState.STATUS_LOGIN_FAILED:
+ case UIState.STATUS_NOT_VERIFIED:
+ this.openPrefsFromFxaMenu("sync_settings", panel);
+ break;
+ case UIState.STATUS_SIGNED_IN:
+ PanelUI.showSubView("PanelUI-fxa-menu-account-panel", panel);
+ }
+ },
+
+ async openFxAEmailFirstPage(entryPoint) {
+ const url = await FxAccounts.config.promiseConnectAccountURI(entryPoint);
+ switchToTabHavingURI(url, true, { replaceQueryString: true });
+ },
+
+ async openFxAEmailFirstPageFromFxaMenu(panel = undefined) {
+ this.emitFxaToolbarTelemetry("login", panel);
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+ this.openFxAEmailFirstPage(entryPoint);
+ },
+
+ async openFxAManagePage(entryPoint) {
+ const url = await FxAccounts.config.promiseManageURI(entryPoint);
+ switchToTabHavingURI(url, true, { replaceQueryString: true });
+ },
+
+ async openFxAManagePageFromFxaMenu(panel = undefined) {
+ this.emitFxaToolbarTelemetry("account_settings", panel);
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+ this.openFxAManagePage(entryPoint);
+ },
+
+ async openSendFromFxaMenu(panel) {
+ this.emitFxaToolbarTelemetry("open_send", panel);
+ this.launchFxaService(gFxaSendLoginUrl);
+ },
+
+ async openMonitorFromFxaMenu(panel) {
+ this.emitFxaToolbarTelemetry("open_monitor", panel);
+ this.launchFxaService(gFxaMonitorLoginUrl);
+ },
+
+ launchFxaService(serviceUrl, panel) {
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+
+ const url = new URL(serviceUrl);
+ url.searchParams.set("utm_source", "fxa-toolbar");
+ url.searchParams.set("utm_medium", "referral");
+ url.searchParams.set("entrypoint", entryPoint);
+
+ const state = UIState.get();
+ if (state.status == UIState.STATUS_SIGNED_IN) {
+ url.searchParams.set("email", state.email);
+ }
+
+ switchToTabHavingURI(url, true, { replaceQueryString: true });
+ },
+
+ // Returns true if we managed to send the tab to any targets, false otherwise.
+ async sendTabToDevice(url, targets, title) {
+ const fxaCommandsDevices = [];
+ const oldSendTabClients = [];
+ for (const target of targets) {
+ if (fxAccounts.commands.sendTab.isDeviceCompatible(target)) {
+ fxaCommandsDevices.push(target);
+ } else if (target.clientRecord) {
+ oldSendTabClients.push(target.clientRecord);
+ } else {
+ this.log.error(`Target ${target.id} unsuitable for send tab.`);
+ }
+ }
+ // If a master-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++;
+ }
+ }
+ for (let client of oldSendTabClients) {
+ try {
+ this.log.info(`Sending a tab to ${client.id} using Sync.`);
+ await Weave.Service.clientsEngine.sendURIToClientForDisplay(
+ url,
+ client.id,
+ title
+ );
+ } catch (e) {
+ numFailed++;
+ this.log.error("Could not send tab to device.", e);
+ }
+ }
+ return numFailed < targets.length; // Good enough.
+ },
+
+ populateSendTabToDevicesMenu(
+ devicesPopup,
+ url,
+ title,
+ multiselected,
+ createDeviceNodeFn
+ ) {
+ if (!createDeviceNodeFn) {
+ createDeviceNodeFn = (targetId, name, targetType, lastModified) => {
+ 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,
+ url,
+ title,
+ multiselected
+ );
+ } else {
+ this._appendSendTabSingleDevice(fragment, createDeviceNodeFn);
+ }
+ } else if (
+ state.status == UIState.STATUS_NOT_VERIFIED ||
+ state.status == UIState.STATUS_LOGIN_FAILED
+ ) {
+ this._appendSendTabVerify(fragment, createDeviceNodeFn);
+ } /* status is STATUS_NOT_CONFIGURED */ else {
+ this._appendSendTabUnconfigured(fragment, createDeviceNodeFn);
+ }
+
+ devicesPopup.appendChild(fragment);
+ },
+
+ _appendSendTabDeviceList(
+ targets,
+ fragment,
+ createDeviceNodeFn,
+ url,
+ title,
+ multiselected
+ ) {
+ 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 => {
+ if (results.includes(true)) {
+ let action = PageActions.actionForID("sendToDevice");
+ showBrowserPageActionFeedback(action);
+ }
+ fxAccounts.flushLogFile();
+ });
+ };
+ const onSendAllCommand = event => {
+ 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 = this.fxaStrings.GetStringFromName(
+ "sendToAllDevices.menuitem"
+ );
+ addTargetDevice("", allDevicesLabel, "");
+
+ // "Manage devices" menu item
+ const manageDevicesLabel = this.fxaStrings.GetStringFromName(
+ "manageDevices.menuitem"
+ );
+ // 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 = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.singledevice.status"
+ );
+ const learnMore = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.singledevice"
+ );
+ const connectDevice = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.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 = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.verify.status"
+ );
+ const verifyAccount = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.verify"
+ );
+ const actions = [
+ { label: verifyAccount, command: () => this.openPrefs("sendtab") },
+ ];
+ this._appendSendTabInfoItems(
+ fragment,
+ createDeviceNodeFn,
+ notVerified,
+ actions
+ );
+ },
+
+ _appendSendTabUnconfigured(fragment, createDeviceNodeFn) {
+ const brandProductName = gBrandBundle.GetStringFromName("brandProductName");
+ const notConnected = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.unconfigured.label2"
+ );
+ const learnMore = this.fxaStrings.GetStringFromName(
+ "sendTabToDevice.unconfigured"
+ );
+ const actions = [
+ { label: learnMore, command: () => this.openSendToDevicePromo() },
+ ];
+ this._appendSendTabInfoItems(
+ fragment,
+ createDeviceNodeFn,
+ notConnected,
+ actions
+ );
+
+ // Now add a 'sign in to Firefox' item above the 'learn more' item.
+ const signInToFxA = this.fxaStrings.formatStringFromName(
+ "sendTabToDevice.signintofxa",
+ [brandProductName]
+ );
+ let signInItem = createDeviceNodeFn(null, signInToFxA, null);
+ signInItem.classList.add("sync-menuitem");
+ signInItem.setAttribute("label", signInToFxA);
+ // Show an icon if opened in the page action panel:
+ if (signInItem.classList.contains("subviewbutton")) {
+ signInItem.classList.add("subviewbutton-iconic", "signintosync");
+ }
+ signInItem.addEventListener("command", () => {
+ this.openPrefs("sendtab");
+ });
+ fragment.insertBefore(signInItem, fragment.lastElementChild);
+ },
+
+ _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(null, null, null);
+ 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);
+ }
+ },
+
+ isSendableURI(aURISpec) {
+ if (!aURISpec) {
+ return false;
+ }
+ // Disallow sending tabs with more than 65535 characters.
+ if (aURISpec.length > 65535) {
+ return false;
+ }
+ if (this.UNSENDABLE_URL_REGEXP) {
+ return !this.UNSENDABLE_URL_REGEXP.test(aURISpec);
+ }
+ // The preference has been removed, or is an invalid regexp, so we treat it
+ // as a valid URI. We've already logged an error when trying to construct
+ // the regexp, and the more problematic case is the length, which we've
+ // already addressed.
+ return true;
+ },
+
+ // "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 (this.isSendableURI(tab.linkedBrowser.currentURI.spec)) {
+ hasASendableURI = true;
+ break;
+ }
+ }
+ const enabled = !this.sendTabConfiguredAndLoading && hasASendableURI;
+
+ let sendTabsToDevice = document.getElementById("context_sendTabToDevice");
+ sendTabsToDevice.disabled = !enabled;
+
+ let tabCount = aTargetTab.multiselected
+ ? gBrowser.multiSelectedTabsCount
+ : 1;
+ sendTabsToDevice.label = PluralForm.get(
+ tabCount,
+ gNavigatorBundle.getString("sendTabsToDevice.label")
+ ).replace("#1", tabCount.toLocaleString());
+ sendTabsToDevice.accessKey = gNavigatorBundle.getString(
+ "sendTabsToDevice.accesskey"
+ );
+ },
+
+ // "Send Page to Device" and "Send Link to Device" menu items
+ updateContentContextMenu(contextMenu) {
+ if (!this.FXA_ENABLED) {
+ // These items are hidden by default. No need to do anything.
+ return;
+ }
+ // showSendLink and showSendPage are mutually exclusive
+ const showSendLink =
+ contextMenu.onSaveableLink || contextMenu.onPlainTextLink;
+ const showSendPage =
+ !showSendLink &&
+ !(
+ contextMenu.isContentSelected ||
+ contextMenu.onImage ||
+ contextMenu.onCanvas ||
+ contextMenu.onVideo ||
+ contextMenu.onAudio ||
+ contextMenu.onLink ||
+ contextMenu.onTextInput
+ );
+
+ // Avoids double separator on images with links.
+ const hideSeparator =
+ contextMenu.isContentSelected &&
+ contextMenu.onLink &&
+ contextMenu.onImage;
+ [
+ "context-sendpagetodevice",
+ ...(hideSeparator ? [] : ["context-sep-sendpagetodevice"]),
+ ].forEach(id => contextMenu.showItem(id, showSendPage));
+ [
+ "context-sendlinktodevice",
+ ...(hideSeparator ? [] : ["context-sep-sendlinktodevice"]),
+ ].forEach(id => contextMenu.showItem(id, showSendLink));
+
+ if (!showSendLink && !showSendPage) {
+ return;
+ }
+
+ const targetURI = showSendLink
+ ? contextMenu.linkURL
+ : contextMenu.browser.currentURI.spec;
+ const enabled =
+ !this.sendTabConfiguredAndLoading && this.isSendableURI(targetURI);
+ contextMenu.setItemAttr(
+ showSendPage ? "context-sendpagetodevice" : "context-sendlinktodevice",
+ "disabled",
+ !enabled || null
+ );
+ },
+
+ // Functions called by observers
+ onActivityStart() {
+ clearTimeout(this._syncAnimationTimer);
+ this._syncStartTime = Date.now();
+
+ document.querySelectorAll(".syncNowBtn").forEach(el => {
+ el.setAttribute("syncstatus", "active");
+ el.setAttribute("disabled", "true");
+ document.l10n.setAttributes(el, el.getAttribute("syncinglabel"));
+ });
+
+ document
+ .getElementById("appMenu-viewCache")
+ .content.querySelectorAll(".syncNowBtn")
+ .forEach(el => {
+ el.setAttribute("syncstatus", "active");
+ el.setAttribute("disabled", "true");
+ document.l10n.setAttributes(el, el.getAttribute("syncinglabel"));
+ });
+ },
+
+ _onActivityStop() {
+ if (!gBrowser) {
+ return;
+ }
+
+ document.querySelectorAll(".syncNowBtn").forEach(el => {
+ el.removeAttribute("syncstatus");
+ el.removeAttribute("disabled");
+ document.l10n.setAttributes(el, "fxa-toolbar-sync-now");
+ });
+
+ document
+ .getElementById("appMenu-viewCache")
+ .content.querySelectorAll(".syncNowBtn")
+ .forEach(el => {
+ el.removeAttribute("syncstatus");
+ el.removeAttribute("disabled");
+ document.l10n.setAttributes(el, "fxa-toolbar-sync-now");
+ });
+
+ Services.obs.notifyObservers(null, "test:browser-sync:activity-stop");
+ },
+
+ onActivityStop() {
+ let now = Date.now();
+ let syncDuration = now - this._syncStartTime;
+
+ if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
+ let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
+ clearTimeout(this._syncAnimationTimer);
+ this._syncAnimationTimer = setTimeout(
+ () => this._onActivityStop(),
+ animationTime
+ );
+ } else {
+ this._onActivityStop();
+ }
+ },
+
+ // Disconnect from sync, and optionally disconnect from the FxA account.
+ // Returns true if the disconnection happened (ie, if the user didn't decline
+ // when asked to confirm)
+ async disconnect({ confirm = true, disconnectAccount = true } = {}) {
+ if (disconnectAccount) {
+ let deleteLocalData = false;
+ if (confirm) {
+ let options = await this._confirmFxaAndSyncDisconnect();
+ if (!options.userConfirmedDisconnect) {
+ return false;
+ }
+ deleteLocalData = options.deleteLocalData;
+ }
+ return this._disconnectFxaAndSync(deleteLocalData);
+ }
+
+ if (confirm && !(await this._confirmSyncDisconnect())) {
+ return false;
+ }
+ return this._disconnectSync();
+ },
+
+ // Prompt the user to confirm disconnect from FxA and sync with the option
+ // to delete syncable data from the device.
+ async _confirmFxaAndSyncDisconnect() {
+ let options = {
+ userConfirmedDisconnect: false,
+ };
+
+ window.openDialog(
+ "chrome://browser/content/browser-fxaSignout.xhtml",
+ "_blank",
+ "chrome,modal,centerscreen,resizable=no",
+ { hideDeleteDataOption: !UIState.get().syncEnabled },
+ options
+ );
+
+ return options;
+ },
+
+ async _disconnectFxaAndSync(deleteLocalData) {
+ const { SyncDisconnect } = ChromeUtils.import(
+ "resource://services-sync/SyncDisconnect.jsm"
+ );
+ // Record telemetry.
+ await fxAccounts.telemetry.recordDisconnection(null, "ui");
+
+ await SyncDisconnect.disconnect(deleteLocalData).catch(e => {
+ console.error("Failed to disconnect.", e);
+ });
+
+ return true;
+ },
+
+ // Prompt the user to confirm disconnect from sync. In this case the data
+ // on the device is not deleted.
+ async _confirmSyncDisconnect() {
+ const l10nPrefix = "sync-disconnect-dialog";
+
+ const [title, body, button] = await document.l10n.formatValues([
+ { id: `${l10nPrefix}-title` },
+ { id: `${l10nPrefix}-body` },
+ { id: "sync-disconnect-dialog-button" },
+ ]);
+
+ const flags =
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1;
+
+ // buttonPressed will be 0 for disconnect, 1 for cancel.
+ const buttonPressed = Services.prompt.confirmEx(
+ window,
+ title,
+ body,
+ flags,
+ button,
+ null,
+ null,
+ null,
+ {}
+ );
+ return buttonPressed == 0;
+ },
+
+ async _disconnectSync() {
+ await fxAccounts.telemetry.recordDisconnection("sync", "ui");
+
+ await Weave.Service.promiseInitialized;
+ await Weave.Service.startOver();
+
+ return true;
+ },
+
+ // doSync forces a sync - it *does not* return a promise as it is called
+ // via the various UI components.
+ doSync() {
+ if (!UIState.isReady()) {
+ return;
+ }
+ // Note we don't bother checking if sync is actually enabled - none of the
+ // UI which calls this function should be visible in that case.
+ const state = UIState.get();
+ if (state.status == UIState.STATUS_SIGNED_IN) {
+ this.updateSyncStatus({ syncing: true });
+ Services.tm.dispatchToMainThread(() => {
+ // We are pretty confident that push helps us pick up all FxA commands,
+ // but some users might have issues with push, so let's unblock them
+ // by fetching the missed FxA commands on manual sync.
+ fxAccounts.commands.pollDeviceCommands().catch(e => {
+ this.log.error("Fetching missed remote commands failed.", e);
+ });
+ Weave.Service.sync();
+ });
+ }
+ },
+
+ doSyncFromFxaMenu(panel) {
+ this.doSync();
+ this.emitFxaToolbarTelemetry("sync_now", panel);
+ },
+
+ openPrefs(entryPoint = "syncbutton", origin = undefined) {
+ window.openPreferences("paneSync", {
+ origin,
+ urlParams: { entrypoint: entryPoint },
+ });
+ },
+
+ openPrefsFromFxaMenu(type, panel) {
+ this.emitFxaToolbarTelemetry(type, panel);
+ let entryPoint = "fxa_discoverability_native";
+ if (this.isPanelInsideAppMenu(panel)) {
+ entryPoint = "fxa_app_menu";
+ }
+ this.openPrefs(entryPoint);
+ },
+
+ openSyncedTabsPanel() {
+ let placement = CustomizableUI.getPlacementOfWidget("sync-button");
+ let area = placement && placement.area;
+ let anchor =
+ document.getElementById("sync-button") ||
+ document.getElementById("PanelUI-menu-button");
+ if (area == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
+ // The button is in the overflow panel, so we need to show the panel,
+ // then show our subview.
+ let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
+ navbar.overflowable.show().then(() => {
+ PanelUI.showSubView("PanelUI-remotetabs", anchor);
+ }, Cu.reportError);
+ } else {
+ // It is placed somewhere else - just try and show it.
+ PanelUI.showSubView("PanelUI-remotetabs", anchor);
+ }
+ },
+
+ refreshSyncButtonsTooltip() {
+ const state = UIState.get();
+ this.updateSyncButtonsTooltip(state);
+ },
+
+ /* Update the tooltip for the sync icon in the main menu and in Synced Tabs.
+ If Sync is configured, the tooltip is when the last sync occurred,
+ otherwise the tooltip reflects the fact that Sync needs to be
+ (re-)configured.
+ */
+ updateSyncButtonsTooltip(state) {
+ const status = state.status;
+
+ // This is a little messy as the Sync buttons are 1/2 Sync related and
+ // 1/2 FxA related - so for some strings we use Sync strings, but for
+ // others we reach into gSync for strings.
+ let tooltiptext;
+ if (status == UIState.STATUS_NOT_VERIFIED) {
+ // "needs verification"
+ tooltiptext = this.fxaStrings.formatStringFromName("verifyDescription", [
+ state.email,
+ ]);
+ } else if (status == UIState.STATUS_NOT_CONFIGURED) {
+ // "needs setup".
+ tooltiptext = this.syncStrings.GetStringFromName(
+ "signInToSync.description"
+ );
+ } else if (status == UIState.STATUS_LOGIN_FAILED) {
+ // "need to reconnect/re-enter your password"
+ tooltiptext = this.fxaStrings.formatStringFromName(
+ "reconnectDescription",
+ [state.email]
+ );
+ } else {
+ // Sync appears configured - format the "last synced at" time.
+ tooltiptext = this.formatLastSyncDate(state.lastSync);
+ }
+
+ document.querySelectorAll(".syncNowBtn").forEach(el => {
+ if (tooltiptext) {
+ el.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ el.removeAttribute("tooltiptext");
+ }
+ });
+
+ document
+ .getElementById("appMenu-viewCache")
+ .content.querySelectorAll(".syncNowBtn")
+ .forEach(el => {
+ if (tooltiptext) {
+ el.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ el.removeAttribute("tooltiptext");
+ }
+ });
+ },
+
+ get relativeTimeFormat() {
+ delete this.relativeTimeFormat;
+ return (this.relativeTimeFormat = new Services.intl.RelativeTimeFormat(
+ undefined,
+ { style: "long" }
+ ));
+ },
+
+ formatLastSyncDate(date) {
+ if (!date) {
+ // Date can be null before the first sync!
+ return null;
+ }
+ try {
+ const relativeDateStr = this.relativeTimeFormat.formatBestUnit(date);
+ return this.syncStrings.formatStringFromName("lastSync2.label", [
+ relativeDateStr,
+ ]);
+ } catch (ex) {
+ // shouldn't happen, but one client having an invalid date shouldn't
+ // break the entire feature.
+ this.log.warn("failed to format lastSync time", date, ex);
+ return null;
+ }
+ },
+
+ onClientsSynced() {
+ // Note that this element is only shown if Sync is enabled.
+ let element = PanelMultiView.getViewNode(
+ document,
+ "PanelUI-remotetabs-main"
+ );
+ if (element) {
+ if (Weave.Service.clientsEngine.stats.numClients > 1) {
+ element.setAttribute("devices-status", "multi");
+ } else {
+ element.setAttribute("devices-status", "single");
+ }
+ }
+ },
+
+ onFxaDisabled() {
+ document.documentElement.setAttribute("fxadisabled", true);
+
+ const toHide = [...document.querySelectorAll(".sync-ui-item")];
+ for (const item of toHide) {
+ item.hidden = true;
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+};