/* 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(
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);
}
},
// "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
--> --------------------