Bug 1826608 - Implement open tabs from other device in new Firefox View. r=sclements,kcochrane,fluent-reviewers,fxview-reviewers,bolsson

Differential Revision: https://phabricator.services.mozilla.com/D180836
This commit is contained in:
Mike Kaply 2023-07-18 19:08:04 +00:00
parent a084b76ea0
commit 134b5be624
18 changed files with 1006 additions and 9 deletions

View File

@ -0,0 +1,30 @@
<!-- 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/. -->
<svg width="156" height="113" fill="none" xmlns="http://www.w3.org/2000/svg">
<g style="mix-blend-mode:luminosity">
<path opacity=".12" d="M75.2 88.827c-23.912-4.903-48.215 17.067-58.61 2.777-7.63-10.544 7.165-18.412-11.284-39.68S21.915 2.628 45.501 13.082 97.069-5.053 121.69.999c11.415 2.836 21.976 11.538 18.709 20.606-1.355 3.749-4.912 7.035-5.253 10.907-.66 7.553 13.624 17.47 15.428 24.873C159.05 92.177 99.111 93.73 75.2 88.827Z" fill="url(#a)"/>
<path stroke="context-stroke" stroke-linecap="round" d="M3.333 76.431h84.166M13.522 80.072h84.166"/>
<path d="M131.941 39.874h4.018c.73 0 1.278.913.73 1.643l-5.296 5.113h-1.643l-5.113-5.295c-.548-.548-.183-1.461.548-1.461h4.017c0-5.844-4.748-10.592-10.591-10.592-2.557 0-4.931.913-6.939 2.557-.548.548-1.461.365-2.009-.183-.183-.365-.183-.547-.183-.913 0-.365.183-.73.548-1.095 2.374-2.009 5.478-3.105 8.765-3.105 7.122 0 13.148 6.026 13.148 13.33ZM109.376 98.527h-4.018c0 5.844 4.748 10.591 10.592 10.591 2.556 0 4.93-.913 6.939-2.556.547-.548 1.461-.365 2.008.183.548.547.366 1.46-.182 2.008-2.374 2.009-5.479 3.105-8.583 3.105-7.304 0-13.33-6.026-13.33-13.33h-4.018c-.73 0-1.278-.914-.73-1.644l5.113-5.113h1.643l5.113 5.295c.548.548.183 1.461-.547 1.461Z" fill="#CB9EFF"/>
<path d="M25.308 77.432h80.247a1.5 1.5 0 0 1 1.258.684l4.964 7.646c.648.998-.068 2.317-1.258 2.317H20.344c-1.19 0-1.906-1.319-1.258-2.317l4.964-7.646a1.5 1.5 0 0 1 1.258-.684Z" fill="context-fill" stroke="context-stroke"/>
<rect x="24.169" y="22.567" width="82.527" height="57.05" rx="6.5" fill="context-fill" stroke="context-stroke"/>
<rect x="27.933" y="26.331" width="74.998" height="49.52" rx="4.375" fill="context-fill" stroke="context-stroke" stroke-width=".75"/>
<path d="M28.308 30.706a4 4 0 0 1 4-4h66.247a4 4 0 0 1 4 4v5.463H28.308v-5.462Z" fill="#CB9EFF"/>
<path stroke="context-stroke" stroke-width=".75" d="M28.308 36.522h74.975"/>
<rect x="31.699" y="29.368" width="19.332" height="4.357" rx=".873" fill="context-fill" stroke="context-stroke" stroke-width=".5"/>
<rect x="55.65" y="29.368" width="19.332" height="4.357" rx=".873" fill="context-fill" stroke="context-stroke" stroke-width=".5"/>
<path stroke="context-stroke" stroke-linecap="round" d="M135.232 98.218h19.269M134.566 100.365h16.885"/>
<rect x="116.823" y="98.075" width="45.403" height="27.933" rx="6.5" transform="rotate(-90 116.823 98.075)" fill="context-fill" stroke="context-stroke"/>
<rect x="119.858" y="95.039" width="39.33" height="21.86" rx="4.375" transform="rotate(-90 119.858 95.039)" fill="context-fill" stroke="context-stroke" stroke-width=".75"/>
<path d="M120.233 63.363v-3.279a4 4 0 0 1 4-4h13.11a4 4 0 0 1 4 4v3.28h-21.11Z" fill="#CB9EFF"/>
<path stroke="context-stroke" stroke-width=".75" d="M120.235 63.716h21.11"/>
<rect x="122.669" y="57.791" width="16.242" height="3.868" rx=".5" fill="context-fill" stroke="context-stroke" stroke-width=".5"/>
<circle cx="131.156" cy="90.296" r="2.662" fill="#FFBDC5" stroke="context-stroke" stroke-width=".5"/>
</g>
<defs>
<linearGradient id="a" x1=".527" y1="49.627" x2="150.86" y2="44.602" gradientUnits="userSpaceOnUse">
<stop stop-color="#7542E4"/>
<stop offset="1" stop-color="#FF9AA2"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -200,6 +200,18 @@ export const TabsSetupFlowManager = new (class {
syncState.syncEnabled syncState.syncEnabled
); );
} }
get currentDevice() {
if (!this.fxaSignedIn) {
return null;
}
let recentDevices = lazy.fxAccounts.device?.recentDeviceList;
if (!recentDevices) {
return null;
}
return recentDevices.find(device => device.isCurrentDevice)?.name;
}
get secondaryDeviceConnected() { get secondaryDeviceConnected() {
if (!this.fxaSignedIn) { if (!this.fxaSignedIn) {
return false; return false;

View File

@ -14,6 +14,7 @@
--fxview-text-primary-color: var(--newtab-text-primary-color, var(--in-content-page-color)); --fxview-text-primary-color: var(--newtab-text-primary-color, var(--in-content-page-color));
--fxview-text-color-hover: var(--newtab-text-primary-color); --fxview-text-color-hover: var(--newtab-text-primary-color);
--fxview-primary-action-background: var(--newtab-primary-action-background, #0061e0); --fxview-primary-action-background: var(--newtab-primary-action-background, #0061e0);
--fxview-border: var(--fc-border-light, #CFCFD8);
/* ensure utility button hover states match those of the rest of the page */ /* ensure utility button hover states match those of the rest of the page */
--in-content-button-background-hover: var(--fxview-element-background-hover); --in-content-button-background-hover: var(--fxview-element-background-hover);

View File

@ -13,6 +13,7 @@
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<title data-l10n-id="firefoxview-page-title"></title> <title data-l10n-id="firefoxview-page-title"></title>
<link rel="localization" href="branding/brand.ftl" /> <link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="toolkit/branding/accounts.ftl" />
<link rel="localization" href="browser/firefoxView.ftl" /> <link rel="localization" href="browser/firefoxView.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" /> <link rel="localization" href="toolkit/branding/brandings.ftl" />
<link rel="localization" href="browser/migrationWizard.ftl" /> <link rel="localization" href="browser/migrationWizard.ftl" />
@ -40,6 +41,10 @@
type="module" type="module"
src="chrome://browser/content/firefoxview/fxview-category-navigation.mjs" src="chrome://browser/content/firefoxview/fxview-category-navigation.mjs"
></script> ></script>
<script
type="module"
src="chrome://browser/content/firefoxview/syncedtabs.mjs"
></script>
<script src="chrome://browser/content/contentTheme.js"></script> <script src="chrome://browser/content/contentTheme.js"></script>
</head> </head>
@ -95,6 +100,7 @@
<view-history name="history"></view-history> <view-history name="history"></view-history>
<view-opentabs name="opentabs"></view-opentabs> <view-opentabs name="opentabs"></view-opentabs>
<view-recentlyclosed name="recentlyclosed"></view-recentlyclosed> <view-recentlyclosed name="recentlyclosed"></view-recentlyclosed>
<view-syncedtabs name="syncedtabs"></view-syncedtabs>
</named-deck> </named-deck>
</main> </main>
<script src="chrome://browser/content/firefoxview/firefoxview-next.mjs"></script> <script src="chrome://browser/content/firefoxview/firefoxview-next.mjs"></script>

View File

@ -29,7 +29,7 @@ class FxviewEmptyState extends MozLitElement {
headerLabel: { type: String }, headerLabel: { type: String },
headerIconUrl: { type: String }, headerIconUrl: { type: String },
isSelectedTab: { type: Boolean }, isSelectedTab: { type: Boolean },
descriptionLabel: { type: Array }, descriptionLabels: { type: Array },
desciptionLink: { type: Object }, desciptionLink: { type: Object },
mainImageUrl: { type: String }, mainImageUrl: { type: String },
}; };
@ -63,12 +63,12 @@ class FxviewEmptyState extends MozLitElement {
this.descriptionLabels, this.descriptionLabels,
(descLabel, index) => html`<p (descLabel, index) => html`<p
class="description ${index !== 0 ? "secondary" : null}" class="description ${index !== 0 ? "secondary" : null}"
data-l10n-id=${descLabel} data-l10n-id="${descLabel}"
> >
<a <a
?hidden=${!this.descriptionLink} ?hidden=${!this.descriptionLink}
data-l10n-name=${this.descriptionLink.name} data-l10n-name=${ifDefined(this.descriptionLink?.name)}
href=${this.descriptionLink.url} href=${ifDefined(this.descriptionLink?.url)}
target="_blank" target="_blank"
/> />
</p>` </p>`

View File

@ -13,6 +13,8 @@ browser.jar:
content/browser/firefoxview/history.mjs content/browser/firefoxview/history.mjs
content/browser/firefoxview/opentabs.mjs content/browser/firefoxview/opentabs.mjs
content/browser/firefoxview/view-opentabs.css content/browser/firefoxview/view-opentabs.css
content/browser/firefoxview/syncedtabs.mjs
content/browser/firefoxview/view-syncedtabs.css
content/browser/firefoxview/overview.mjs content/browser/firefoxview/overview.mjs
content/browser/firefoxview/firefoxview.css content/browser/firefoxview/firefoxview.css
content/browser/firefoxview/firefoxview-next.css content/browser/firefoxview/firefoxview-next.css
@ -38,6 +40,7 @@ browser.jar:
content/browser/firefoxview/category-recentlyclosed.svg (content/category-recentlyclosed.svg) content/browser/firefoxview/category-recentlyclosed.svg (content/category-recentlyclosed.svg)
content/browser/firefoxview/category-syncedtabs.svg (content/category-syncedtabs.svg) content/browser/firefoxview/category-syncedtabs.svg (content/category-syncedtabs.svg)
content/browser/firefoxview/tab-pickup-empty.svg (content/tab-pickup-empty.svg) content/browser/firefoxview/tab-pickup-empty.svg (content/tab-pickup-empty.svg)
content/browser/firefoxview/synced-tabs-error.svg (content/synced-tabs-error.svg)
content/browser/callout-tab-pickup.svg (content/callout-tab-pickup.svg) content/browser/callout-tab-pickup.svg (content/callout-tab-pickup.svg)
content/browser/callout-tab-pickup-dark.svg (content/callout-tab-pickup-dark.svg) content/browser/callout-tab-pickup-dark.svg (content/callout-tab-pickup-dark.svg)
content/browser/cfr-lightning.svg (content/cfr-lightning.svg) content/browser/cfr-lightning.svg (content/cfr-lightning.svg)

View File

@ -0,0 +1,440 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
});
const { SyncedTabsErrorHandler } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs"
);
const { TabsSetupFlowManager } = ChromeUtils.importESModule(
"resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"
);
import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
import { ViewPage } from "./viewpage.mjs";
const SYNCED_TABS_CHANGED = "services.sync.tabs.changed";
const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open";
class SyncedTabsInView extends ViewPage {
constructor() {
super();
this.boundObserve = (...args) => this.observe(...args);
this._currentSetupStateIndex = -1;
this.errorState = null;
this._id = Math.floor(Math.random() * 10e6);
this.currentSyncedTabs = [];
if (this.overview) {
this.maxTabsLength = 5;
} else {
// Setting maxTabsLength to -1 for no max
this.maxTabsLength = -1;
}
this.devices = [];
}
static properties = {
...ViewPage.properties,
errorState: { type: Number },
currentSyncedTabs: { type: Array },
_currentSetupStateIndex: { type: Number },
devices: { type: Array },
};
connectedCallback() {
super.connectedCallback();
this.addEventListener("click", this);
this.ownerDocument.addEventListener("visibilitychange", this);
Services.obs.addObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED);
Services.obs.addObserver(this.boundObserve, SYNCED_TABS_CHANGED);
this.updateStates();
this.onVisibilityChange();
}
cleanup() {
TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded");
this.ownerDocument?.removeEventListener("visibilitychange", this);
Services.obs.removeObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED);
Services.obs.removeObserver(this.boundObserve, SYNCED_TABS_CHANGED);
}
disconnectedCallback() {
this.cleanup();
}
handleEvent(event) {
if (event.type == "click" && event.target.dataset.action) {
const { ErrorType } = SyncedTabsErrorHandler;
switch (event.target.dataset.action) {
case `${ErrorType.SYNC_ERROR}`:
case `${ErrorType.NETWORK_OFFLINE}`:
case `${ErrorType.PASSWORD_LOCKED}`: {
TabsSetupFlowManager.tryToClearError();
break;
}
case `${ErrorType.SIGNED_OUT}`:
case "sign-in": {
TabsSetupFlowManager.openFxASignup(event.target.ownerGlobal);
break;
}
case "add-device": {
TabsSetupFlowManager.openFxAPairDevice(event.target.ownerGlobal);
break;
}
case "sync-tabs-disabled": {
TabsSetupFlowManager.syncOpenTabs(event.target);
break;
}
case `${ErrorType.SYNC_DISCONNECTED}`: {
const win = event.target.ownerGlobal;
const { switchToTabHavingURI } =
win.docShell.chromeEventHandler.ownerGlobal;
switchToTabHavingURI(
"about:preferences?action=choose-what-to-sync#sync",
true,
{}
);
break;
}
}
}
if (event.type == "change") {
TabsSetupFlowManager.syncOpenTabs(event.target);
}
// Returning to fxview seems like a likely time for a device check
if (event.type == "visibilitychange") {
this.onVisibilityChange();
}
}
onVisibilityChange() {
const isVisible = document.visibilityState == "visible";
const isOpen = this.open;
if (isVisible && isOpen) {
this.update();
TabsSetupFlowManager.updateViewVisibility(this._id, "visible");
} else {
TabsSetupFlowManager.updateViewVisibility(
this._id,
isVisible ? "closed" : "hidden"
);
}
}
async observe(subject, topic, errorState) {
if (topic == TOPIC_SETUPSTATE_CHANGED) {
this.updateStates({ errorState });
}
if (topic == SYNCED_TABS_CHANGED) {
this.getSyncedTabData();
}
}
updateStates({
stateIndex = TabsSetupFlowManager.uiStateIndex,
errorState = SyncedTabsErrorHandler.getErrorType(),
} = {}) {
if (stateIndex == 4 && this._currentSetupStateIndex !== stateIndex) {
// trigger an initial request for the synced tabs list
this.getSyncedTabData();
}
this._currentSetupStateIndex = stateIndex;
this.errorState = errorState;
}
actionMappings = {
"sign-in": {
header: "firefoxview-syncedtabs-signin-header",
description: "firefoxview-syncedtabs-signin-description",
buttonLabel: "firefoxview-syncedtabs-signin-primarybutton",
},
"add-device": {
header: "firefoxview-syncedtabs-adddevice-header",
description: "firefoxview-syncedtabs-adddevice-description",
buttonLabel: "firefoxview-syncedtabs-adddevice-primarybutton",
link: "https://support.mozilla.org/kb/how-do-i-set-sync-my-computer#w_connect-additional-devices-to-sync",
},
"sync-tabs-disabled": {
header: "firefoxview-syncedtabs-synctabs-header",
description: "firefoxview-syncedtabs-synctabs-description",
checkboxLabel: "firefoxview-syncedtabs-synctabs-checkbox",
},
};
generateMessageCard({ error = false, action, errorState }) {
errorState = errorState || this.errorState;
let header,
description,
descriptionLink,
buttonLabel,
checkboxLabel,
headerIconUrl,
mainImageUrl;
let descriptionArray;
if (error) {
let link;
({ header, description, link, buttonLabel } =
SyncedTabsErrorHandler.getFluentStringsForErrorType(errorState));
action = `${errorState}`;
headerIconUrl = "chrome://global/skin/icons/info-filled.svg";
mainImageUrl =
"chrome://browser/content/firefoxview/synced-tabs-error.svg";
descriptionArray = [description];
if (errorState == "password-locked") {
descriptionLink = {};
// This is ugly, but we need to special case this link so we can
// coexist with the old view.
descriptionArray.push("firefoxview-syncedtab-password-locked-link");
descriptionLink.name = "syncedtab-password-locked-link";
descriptionLink.url = link.href;
}
} else {
header = this.actionMappings[action].header;
description = this.actionMappings[action].description;
buttonLabel = this.actionMappings[action].buttonLabel;
checkboxLabel = this.actionMappings[action].checkboxLabel;
descriptionLink = this.actionMappings[action];
mainImageUrl =
"chrome://browser/content/firefoxview/synced-tabs-error.svg";
descriptionArray = [description];
}
return html`
<fxview-empty-state
headerLabel=${header}
.descriptionLabels=${descriptionArray}
.descriptionLink=${ifDefined(descriptionLink)}
class="empty-state synced-tabs"
?isSelectedTab=${this.selectedTab}
mainImageUrl="${ifDefined(mainImageUrl)}"
headerIconUrl="${ifDefined(headerIconUrl)}"
>
<button
class="primary"
slot="primary-action"
?hidden=${!buttonLabel}
data-l10n-id="${ifDefined(buttonLabel)}"
data-action="${action}"
@click=${this.handleEvent}
></button>
<div slot="primary-action"
?hidden=${!checkboxLabel} >
<label>
<input type="checkbox" @change=${this.handleEvent}></input>
<span data-l10n-id="${ifDefined(checkboxLabel)}"></span>
</label>
</div>
</fxview-empty-state>
`;
}
onOpenLink(event) {
let currentWindow = this.getWindow();
if (currentWindow.openTrustedLinkIn) {
let where = lazy.BrowserUtils.whereToOpenLink(
event.detail.originalEvent,
false,
true
);
if (where == "current") {
where = "tab";
}
currentWindow.openTrustedLinkIn(event.originalTarget.url, where);
}
}
onContextMenu(event) {
//TODO bug 1833664
}
noDeviceTabsTemplate(deviceName, deviceType) {
return html`<card-container>
<h2 slot="header">
<div class="icon ${deviceType}" role="presentation"></div>
${deviceName}
</h2>
<div slot="main" class="blackbox notabs">No tabs open on this device</div>
</card-container>`;
}
generateTabList() {
let renderArray = [];
let renderInfo = {};
for (let tab of this.currentSyncedTabs) {
if (!(tab.device in renderInfo)) {
renderInfo[tab.device] = {
deviceType: tab.deviceType,
tabs: [],
};
}
renderInfo[tab.device].tabs.push(tab);
}
// Add devices without tabs
let currentDevice = TabsSetupFlowManager.currentDevice;
for (let device of this.devices) {
if (device.name == currentDevice) {
continue;
}
if (!(device.name in renderInfo)) {
renderInfo[device.name] = {
deviceType: device.type,
tabs: [],
};
}
}
for (let deviceName in renderInfo) {
if (renderInfo[deviceName].tabs.length) {
renderArray.push(html`<card-container>
<h2 slot="header">
<div
class="icon ${renderInfo[deviceName].deviceType}"
role="presentation"
></div>
${deviceName}
</h2>
<fxview-tab-list
slot="main"
.tabItems=${ifDefined(
this.getTabItems(renderInfo[deviceName].tabs)
)}
maxTabsLength=${this.maxTabsLength}
@fxview-tab-list-secondary-action=${this.onContextMenu}
@fxview-tab-list-primary-action=${this.onOpenLink}
></fxview-tab-list>
</card-container>`);
} else {
renderArray.push(
this.noDeviceTabsTemplate(
deviceName,
renderInfo[deviceName].deviceType
)
);
}
}
return renderArray;
}
render() {
const stateIndex = this._currentSetupStateIndex;
this.open =
!TabsSetupFlowManager.isTabSyncSetupComplete ||
Services.prefs.getBoolPref(UI_OPEN_STATE, true);
let renderArray = [];
renderArray.push(html` <link
rel="stylesheet"
href="chrome://browser/content/firefoxview/view-syncedtabs.css"
/>`);
renderArray.push(html` <link
rel="stylesheet"
href="chrome://browser/content/firefoxview/firefoxview-next.css"
/>`);
if (!this.overview) {
renderArray.push(html`<div class="sticky-container bottom-fade">
<h2
class="page-header"
data-l10n-id="firefoxview-synced-tabs-header"
></h2>
</div>`);
}
switch (stateIndex) {
case 0 /* error-state */:
if (this.errorState) {
renderArray.push(this.generateMessageCard({ error: true }));
}
break;
case 1 /* not-signed-in */:
if (Services.prefs.prefHasUserValue("services.sync.lastversion")) {
// If this pref is set, the user has signed out of sync.
// This path is also taken if we are disconnected from sync. See bug 1784055
renderArray.push(
this.generateMessageCard({ error: true, errorState: "signed-out" })
);
} else {
renderArray.push(this.generateMessageCard({ action: "sign-in" }));
}
break;
case 2 /* connect-secondary-device*/:
renderArray.push(this.generateMessageCard({ action: "add-device" }));
break;
case 3 /* disabled-tab-sync */:
renderArray.push(
this.generateMessageCard({ action: "sync-tabs-disabled" })
);
break;
case 4 /* synced-tabs-loaded*/:
renderArray = renderArray.concat(this.generateTabList());
break;
}
return renderArray;
}
async onReload() {
await TabsSetupFlowManager.syncOnPageReload();
}
getTabItems(tabs) {
tabs = tabs || this.tabs;
return tabs?.map(tab => ({
icon: tab.icon,
title: tab.title,
time: tab.lastUsed * 1000,
url: tab.url,
primaryL10nId: "firefoxview-tabs-list-tab-button",
primaryL10nArgs: JSON.stringify({ targetURI: tab.url }),
secondaryL10nId: "firefoxview-close-button",
}));
}
updateTabsList(syncedTabs) {
if (!syncedTabs.length) {
this.currentSyncedTabs = syncedTabs;
this.sendTabTelemetry(0);
}
const tabsToRender = syncedTabs;
// Return early if new tabs are the same as previous ones
if (
JSON.stringify(tabsToRender) == JSON.stringify(this.currentSyncedTabs)
) {
return;
}
this.currentSyncedTabs = tabsToRender;
// Record the full tab count
this.sendTabTelemetry(syncedTabs.length);
}
async getSyncedTabData() {
this.devices = await lazy.SyncedTabs.getTabClients();
let tabs = await lazy.SyncedTabs.getRecentTabs(50, {
removeAllDupes: false,
removeDeviceDupes: true,
});
this.updateTabsList(tabs);
}
sendTabTelemetry(numTabs) {
Services.telemetry.recordEvent(
"firefoxview-next",
"synced_tabs",
"tabs",
null,
{
count: numTabs.toString(),
}
);
}
}
customElements.define("view-syncedtabs", SyncedTabsInView);

View File

@ -30,6 +30,8 @@ skip-if = true # Bug 1783684
[browser_setup_state.js] [browser_setup_state.js]
[browser_setup_synced_tabs_loading.js] [browser_setup_synced_tabs_loading.js]
[browser_sync_admin_disabled.js] [browser_sync_admin_disabled.js]
[browser_syncedtabs_errors_firefoxview_next.js]
[browser_syncedtabs_firefoxview_next.js]
[browser_tab_close_last_tab.js] [browser_tab_close_last_tab.js]
[browser_tab_on_close_warning.js] [browser_tab_on_close_warning.js]
[browser_tab_pickup_device_added_telemetry.js] [browser_tab_pickup_device_added_telemetry.js]

View File

@ -17,11 +17,13 @@ function setupRecentDeviceListMocks() {
name: "My desktop", name: "My desktop",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
{ {
id: 2, id: 2,
name: "My iphone", name: "My iphone",
type: "mobile", type: "mobile",
tabs: [],
}, },
]); ]);

View File

@ -14,11 +14,13 @@ async function setupWithDesktopDevices(state = UIState.STATUS_SIGNED_IN) {
name: "This Device", name: "This Device",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
{ {
id: 2, id: 2,
name: "Other Device", name: "Other Device",
type: "desktop", type: "desktop",
tabs: [],
}, },
], ],
}); });

View File

@ -32,11 +32,13 @@ async function setupWithDesktopDevices() {
name: "This Device", name: "This Device",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
{ {
id: 2, id: 2,
name: "Other Device", name: "Other Device",
type: "desktop", type: "desktop",
tabs: [],
}, },
], ],
}); });
@ -119,6 +121,7 @@ add_task(async function test_signed_in() {
name: "This Device", name: "This Device",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
], ],
}); });
@ -176,6 +179,7 @@ add_task(async function test_support_links() {
name: "This Device", name: "This Device",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
], ],
}); });
@ -202,11 +206,13 @@ add_task(async function test_2nd_desktop_connected() {
name: "This Device", name: "This Device",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
{ {
id: 2, id: 2,
name: "Other Device", name: "Other Device",
type: "desktop", type: "desktop",
tabs: [],
}, },
], ],
}); });
@ -246,11 +252,13 @@ add_task(async function test_mobile_connected() {
name: "This Device", name: "This Device",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
{ {
id: 2, id: 2,
name: "Other Device", name: "Other Device",
type: "mobile", type: "mobile",
tabs: [],
}, },
], ],
}); });
@ -290,11 +298,13 @@ add_task(async function test_tablet_connected() {
name: "This Device", name: "This Device",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
{ {
id: 2, id: 2,
name: "Other Device", name: "Other Device",
type: "tablet", type: "tablet",
tabs: [],
}, },
], ],
}); });
@ -334,11 +344,13 @@ add_task(async function test_tab_sync_enabled() {
name: "This Device", name: "This Device",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
{ {
id: 2, id: 2,
name: "Other Device", name: "Other Device",
type: "mobile", type: "mobile",
tabs: [],
}, },
], ],
}); });
@ -413,6 +425,7 @@ add_task(async function test_mobile_promo() {
id: 3, id: 3,
name: "Mobile Device", name: "Mobile Device",
type: "mobile", type: "mobile",
tabs: [],
}); });
Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated"); Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated");
@ -555,6 +568,7 @@ add_task(async function test_mobile_promo_windows() {
id: 3, id: 3,
name: "Mobile Device", name: "Mobile Device",
type: "mobile", type: "mobile",
tabs: [],
}); });
Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated"); Services.obs.notifyObservers(null, "fxaccounts:devicelist_updated");
@ -711,6 +725,7 @@ add_task(async function test_close_device_connected_tab() {
name: "This Device", name: "This Device",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
], ],
}); });

View File

@ -0,0 +1,121 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const { LoginTestUtils } = ChromeUtils.importESModule(
"resource://testing-common/LoginTestUtils.sys.mjs"
);
async function setupWithDesktopDevices(state = UIState.STATUS_SIGNED_IN) {
const sandbox = setupSyncFxAMocks({
state,
fxaDevices: [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
type: "desktop",
tabs: [],
},
{
id: 2,
name: "Other Device",
type: "desktop",
tabs: [],
},
],
});
return sandbox;
}
async function tearDown(sandbox) {
sandbox?.restore();
Services.prefs.clearUserPref("services.sync.lastTabFetch");
Services.prefs.clearUserPref("identity.fxaccounts.enabled");
}
add_setup(async function () {
// gSync.init() is called in a requestIdleCallback. Force its initialization.
gSync.init();
await SpecialPowers.pushPrefEnv({
set: [
["browser.tabs.firefox-view-next", true],
["services.sync.engine.tabs", true],
["identity.fxaccounts.enabled", true],
],
});
registerCleanupFunction(async function () {
// reset internal state so it doesn't affect the next tests
TabsSetupFlowManager.resetInternalState();
await tearDown(gSandbox);
});
});
add_task(async function test_network_offline() {
const sandbox = await setupWithDesktopDevices();
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
Services.obs.notifyObservers(
null,
"network:offline-status-changed",
"offline"
);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
);
await BrowserTestUtils.waitForMutationCondition(
syncedTabsComponent.shadowRoot,
{ childList: true },
() => syncedTabsComponent.shadowRoot.innerHTML.includes("network-offline")
);
let emptyState =
syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
emptyState.getAttribute("headerlabel").includes("network-offline"),
"Network offline message is shown"
);
Services.obs.notifyObservers(
null,
"network:offline-status-changed",
"online"
);
});
await tearDown(sandbox);
});
add_task(async function test_sync_error() {
const sandbox = await setupWithDesktopDevices();
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
Services.obs.notifyObservers(null, "weave:service:sync:error");
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
);
await BrowserTestUtils.waitForMutationCondition(
syncedTabsComponent.shadowRoot,
{ childList: true },
() => syncedTabsComponent.shadowRoot.innerHTML.includes("sync-error")
);
let emptyState =
syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
emptyState.getAttribute("headerlabel").includes("sync-error"),
"Correct message should show when there's a sync service error"
);
// Clear the error.
Services.obs.notifyObservers(null, "weave:service:sync:finish");
});
await tearDown(sandbox);
});

View File

@ -0,0 +1,267 @@
add_setup(async function () {
registerCleanupFunction(() => {
// reset internal state so it doesn't affect the next tests
TabsSetupFlowManager.resetInternalState();
});
// gSync.init() is called in a requestIdleCallback. Force its initialization.
gSync.init();
registerCleanupFunction(async function () {
await tearDown(gSandbox);
});
await SpecialPowers.pushPrefEnv({
set: [["browser.tabs.firefox-view-next", true]],
});
});
add_task(async function test_unconfigured_initial_state() {
const sandbox = setupMocks({
state: UIState.STATUS_NOT_CONFIGURED,
syncEnabled: false,
});
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
);
let emptyState =
syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
emptyState.getAttribute("headerlabel").includes("syncedtabs-signin"),
"Signin message is shown"
);
});
await tearDown(sandbox);
});
add_task(async function test_signed_in() {
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
type: "desktop",
tabs: [],
},
],
});
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
);
await syncedTabsComponent.updateComplete;
let emptyState =
syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
emptyState.getAttribute("headerlabel").includes("syncedtabs-adddevice"),
"Add device message is shown"
);
});
await tearDown(sandbox);
});
add_task(async function test_no_synced_tabs() {
Services.prefs.setBoolPref("services.sync.engine.tabs", false);
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
type: "desktop",
tabs: [],
},
{
id: 2,
name: "Other Device",
type: "desktop",
tabs: [],
},
],
});
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
);
await syncedTabsComponent.updateComplete;
let emptyState =
syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
ok(
emptyState.getAttribute("headerlabel").includes("syncedtabs-synctabs"),
"Enable synced tabs message is shown"
);
});
await tearDown(sandbox);
Services.prefs.setBoolPref("services.sync.engine.tabs", true);
});
add_task(async function test_no_error_for_two_desktop() {
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
clientType: "desktop",
tabs: [],
},
{
id: 2,
name: "Other Device",
clientType: "desktop",
tabs: [],
},
],
});
await withFirefoxView({}, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
);
await syncedTabsComponent.updateComplete;
// I don't love this, but I'm out of ideas
await TestUtils.waitForTick();
let emptyState =
syncedTabsComponent.shadowRoot.querySelector("fxview-empty-state");
is(emptyState, null, "No empty state should be shown");
let noTabs = syncedTabsComponent.shadowRoot.querySelectorAll(".notabs");
is(noTabs.length, 1, "Should be 1 empty device");
});
await tearDown(sandbox);
});
add_task(async function test_empty_state() {
const sandbox = setupMocks({
state: UIState.STATUS_SIGNED_IN,
fxaDevices: [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
type: "desktop",
tabs: [],
},
{
id: 2,
name: "Other Desktop",
type: "desktop",
tabs: [],
},
{
id: 3,
name: "Other Mobile",
type: "phone",
tabs: [],
},
],
});
await withFirefoxView({ openNewWindow: true }, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
);
await syncedTabsComponent.updateComplete;
// I don't love this, but I'm out of ideas
await TestUtils.waitForTick();
let noTabs = syncedTabsComponent.shadowRoot.querySelectorAll(".notabs");
is(noTabs.length, 2, "Should be 2 empty devices");
let headers =
syncedTabsComponent.shadowRoot.querySelectorAll("h2[slot=header]");
ok(
headers[0].textContent.includes("Other Desktop"),
"Text is correct (Desktop)"
);
ok(headers[0].innerHTML.includes("icon desktop"), "Icon should be desktop");
ok(
headers[1].textContent.includes("Other Mobile"),
"Text is correct (Mobile)"
);
ok(headers[1].innerHTML.includes("icon phone"), "Icon should be phone");
});
await tearDown(sandbox);
});
add_task(async function test_tabs() {
TabsSetupFlowManager.resetInternalState();
const sandbox = setupRecentDeviceListMocks();
const syncedTabsMock = sandbox.stub(SyncedTabs, "getRecentTabs");
let mockTabs1 = getMockTabData(syncedTabsData1);
let getRecentTabsResult = mockTabs1;
syncedTabsMock.callsFake(() => {
info(
`Stubbed SyncedTabs.getRecentTabs returning a promise that resolves to ${getRecentTabsResult.length} tabs\n`
);
return Promise.resolve(getRecentTabsResult);
});
await withFirefoxView({ openNewWindow: true }, async browser => {
const { document } = browser.contentWindow;
navigateToCategory(document, "syncedtabs");
Services.obs.notifyObservers(null, UIState.ON_UPDATE);
let syncedTabsComponent = document.querySelector(
"view-syncedtabs:not([slot=syncedtabs])"
);
await syncedTabsComponent.updateComplete;
// I don't love this, but I'm out of ideas
await TestUtils.waitForTick();
let headers =
syncedTabsComponent.shadowRoot.querySelectorAll("h2[slot=header]");
ok(
headers[0].textContent.includes("My desktop"),
"Text is correct (My desktop)"
);
ok(headers[0].innerHTML.includes("icon desktop"), "Icon should be desktop");
ok(
headers[1].textContent.includes("My iphone"),
"Text is correct (My iphone)"
);
ok(headers[1].innerHTML.includes("icon phone"), "Icon should be phone");
let tabLists =
syncedTabsComponent.shadowRoot.querySelectorAll("fxview-tab-list");
let tabRow1 = tabLists[0].shadowRoot.querySelectorAll("fxview-tab-row");
ok(
tabRow1[0].shadowRoot.textContent.includes,
"Internet for people, not profits - Mozilla"
);
ok(tabRow1[1].shadowRoot.textContent.includes, "Sandboxes - Sinon.JS");
is(tabRow1.length, 2, "Correct number of rows are displayed.");
let tabRow2 = tabLists[1].shadowRoot.querySelectorAll("fxview-tab-row");
is(tabRow2.length, 2, "Correct number of rows are dispayed.");
ok(tabRow1[0].shadowRoot.textContent.includes, "The Guardian");
ok(tabRow1[1].shadowRoot.textContent.includes, "The Times");
});
await tearDown(sandbox);
});

View File

@ -15,12 +15,14 @@ function setupWithFxaDevices() {
name: "My desktop", name: "My desktop",
isCurrentDevice: true, isCurrentDevice: true,
type: "desktop", type: "desktop",
tabs: [],
}, },
{ {
id: 2, id: 2,
name: "Other device", name: "Other device",
isCurrentDevice: false, isCurrentDevice: false,
type: "mobile", type: "mobile",
tabs: [],
}, },
], ],
})); }));

View File

@ -243,7 +243,7 @@ function setupRecentDeviceListMocks() {
} }
function getMockTabData(clients) { function getMockTabData(clients) {
return SyncedTabs._internal._createRecentTabsList(clients, 3); return SyncedTabs._internal._createRecentTabsList(clients, 10);
} }
async function setupListState(browser) { async function setupListState(browser) {
@ -336,6 +336,9 @@ function setupMocks({ fxaDevices = null, state, syncEnabled = true }) {
}), }),
}; };
}); });
sandbox.stub(SyncedTabs, "getTabClients").callsFake(() => {
return Promise.resolve(fxaDevices);
});
return sandbox; return sandbox;
} }
@ -570,3 +573,13 @@ registerCleanupFunction(() => {
// that might have prevented it // that might have prevented it
gSandbox?.restore(); gSandbox?.restore();
}); });
function navigateToCategory(document, category) {
const navigation = document.querySelector("fxview-category-navigation");
let navButton = Array.from(navigation.categoryButtons).filter(
categoryButton => {
return categoryButton.name === category;
}
)[0];
navButton.buttonEl.click();
}

View File

@ -0,0 +1,59 @@
/* 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/. */
@import url("chrome://global/skin/in-content/common.css");
.icon {
vertical-align: bottom;
margin-inline-end: 5px;
display: inline-block;
width: 16px;
height: 16px;
background-position: center center;
background-repeat: no-repeat;
-moz-context-properties: fill;
fill: currentColor;
}
.phone, .mobile {
background-image: url('chrome://browser/skin/device-phone.svg');
}
.desktop {
background-image: url('chrome://browser/skin/device-desktop.svg');
}
.tablet {
background-image: url('chrome://browser/skin/device-tablet.svg');
}
h2 {
display: flex;
align-items: center;
}
.notabs {
margin-top: 15px;
}
.blackbox {
border: 1px solid var(--fxview-border);
text-align: center;
height: 70px;
border-radius: 8px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
button.primary {
white-space: nowrap;
min-width: fit-content;
}
label {
display: flex;
align-items: center;
}

View File

@ -31,16 +31,28 @@ firefoxview-tabpickup-step-signin-header = Switch seamlessly between devices
firefoxview-tabpickup-step-signin-description = To grab your phone tabs here, first sign in or create an account. firefoxview-tabpickup-step-signin-description = To grab your phone tabs here, first sign in or create an account.
firefoxview-tabpickup-step-signin-primarybutton = Continue firefoxview-tabpickup-step-signin-primarybutton = Continue
firefoxview-syncedtabs-signin-header = Grab tabs from anywhere
firefoxview-syncedtabs-signin-description = To see your tabs from wherever you use { -brand-product-name }, sign in to your account. If you dont have an account, well take you through the steps to sign up.
firefoxview-syncedtabs-signin-primarybutton = Sign in or sign up
firefoxview-tabpickup-adddevice-header = Sync { -brand-product-name } on your phone or tablet firefoxview-tabpickup-adddevice-header = Sync { -brand-product-name } on your phone or tablet
firefoxview-tabpickup-adddevice-description = Download { -brand-product-name } for mobile and sign in there. firefoxview-tabpickup-adddevice-description = Download { -brand-product-name } for mobile and sign in there.
firefoxview-tabpickup-adddevice-learn-how = Learn how firefoxview-tabpickup-adddevice-learn-how = Learn how
firefoxview-tabpickup-adddevice-primarybutton = Get { -brand-product-name } for mobile firefoxview-tabpickup-adddevice-primarybutton = Get { -brand-product-name } for mobile
firefoxview-syncedtabs-adddevice-header = Sign in to { -brand-product-name } on your other devices
firefoxview-syncedtabs-adddevice-description = To see your tabs from wherever you use { -brand-product-name }, sign in on all your devices. Learn how to <a data-l10n-name="url">connect additional devices</a>.
firefoxview-syncedtabs-adddevice-primarybutton = Try { -brand-product-name } for mobile
firefoxview-tabpickup-synctabs-header = Turn on tab syncing firefoxview-tabpickup-synctabs-header = Turn on tab syncing
firefoxview-tabpickup-synctabs-description = Allow { -brand-short-name } to share tabs between devices. firefoxview-tabpickup-synctabs-description = Allow { -brand-short-name } to share tabs between devices.
firefoxview-tabpickup-synctabs-learn-how = Learn how firefoxview-tabpickup-synctabs-learn-how = Learn how
firefoxview-tabpickup-synctabs-primarybutton = Sync open tabs firefoxview-tabpickup-synctabs-primarybutton = Sync open tabs
firefoxview-syncedtabs-synctabs-header = Update your sync settings
firefoxview-syncedtabs-synctabs-description = To see tabs from other devices, you need to sync your open tabs.
firefoxview-syncedtabs-synctabs-checkbox = Allow open tabs to sync
firefoxview-tabpickup-fxa-admin-disabled-header = Your organization has disabled sync firefoxview-tabpickup-fxa-admin-disabled-header = Your organization has disabled sync
firefoxview-tabpickup-fxa-admin-disabled-description = { -brand-short-name } is not able to sync tabs between devices because your administrator has disabled syncing. firefoxview-tabpickup-fxa-admin-disabled-description = { -brand-short-name } is not able to sync tabs between devices because your administrator has disabled syncing.
@ -60,6 +72,7 @@ firefoxview-tabpickup-password-locked-header = Enter your Primary Password to vi
firefoxview-tabpickup-password-locked-description = To grab your tabs, youll need to enter the Primary Password for { -brand-short-name }. firefoxview-tabpickup-password-locked-description = To grab your tabs, youll need to enter the Primary Password for { -brand-short-name }.
firefoxview-tabpickup-password-locked-link = Learn more firefoxview-tabpickup-password-locked-link = Learn more
firefoxview-tabpickup-password-locked-primarybutton = Enter Primary Password firefoxview-tabpickup-password-locked-primarybutton = Enter Primary Password
firefoxview-syncedtab-password-locked-link = <a data-l10n-name="syncedtab-password-locked-link">Learn more</a>
firefoxview-tabpickup-signed-out-header = Sign in to reconnect firefoxview-tabpickup-signed-out-header = Sign in to reconnect
firefoxview-tabpickup-signed-out-description = To reconnect and grab your tabs, sign in to your { -fxaccount-brand-name }. firefoxview-tabpickup-signed-out-description = To reconnect and grab your tabs, sign in to your { -fxaccount-brand-name }.

View File

@ -81,17 +81,26 @@ let SyncedTabsInternal = {
return reFilter.test(tab.url) || reFilter.test(tab.title); return reFilter.test(tab.url) || reFilter.test(tab.title);
}, },
_createRecentTabsList(clients, maxCount) { _createRecentTabsList(
clients,
maxCount,
extraParams = { removeAllDupes: true, removeDeviceDupes: false }
) {
let tabs = []; let tabs = [];
for (let client of clients) { for (let client of clients) {
if (extraParams.removeDeviceDupes) {
client.tabs = this._filterRecentTabsDupes(client.tabs);
}
for (let tab of client.tabs) { for (let tab of client.tabs) {
tab.device = client.name; tab.device = client.name;
tab.deviceType = client.clientType; tab.deviceType = client.clientType;
} }
tabs = [...tabs, ...client.tabs.reverse()]; tabs = [...tabs, ...client.tabs.reverse()];
} }
tabs = this._filterRecentTabsDupes(tabs); if (extraParams.removeAllDupes) {
tabs = this._filterRecentTabsDupes(tabs);
}
tabs = tabs.sort((a, b) => b.lastUsed - a.lastUsed).slice(0, maxCount); tabs = tabs.sort((a, b) => b.lastUsed - a.lastUsed).slice(0, maxCount);
return tabs; return tabs;
}, },
@ -327,8 +336,8 @@ export var SyncedTabs = {
// Get list of synced tabs across all devices/clients // Get list of synced tabs across all devices/clients
// truncated by value of maxCount param, sorted by // truncated by value of maxCount param, sorted by
// lastUsed value, and filtered for duplicate URLs // lastUsed value, and filtered for duplicate URLs
async getRecentTabs(maxCount) { async getRecentTabs(maxCount, extraParams) {
let clients = await this.getTabClients(); let clients = await this.getTabClients();
return this._internal._createRecentTabsList(clients, maxCount); return this._internal._createRecentTabsList(clients, maxCount, extraParams);
}, },
}; };