diff --git a/browser/components/firefoxview/SyncedTabsController.sys.mjs b/browser/components/firefoxview/SyncedTabsController.sys.mjs index abe5c81a9bc6..2e8458829c45 100644 --- a/browser/components/firefoxview/SyncedTabsController.sys.mjs +++ b/browser/components/firefoxview/SyncedTabsController.sys.mjs @@ -267,8 +267,8 @@ export class SyncedTabsController { for (let id in renderInfo) { renderInfo[id].tabItems = this.searchQuery - ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs)) - : this.getTabItems(renderInfo[id].tabs); + ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id])) + : this.getTabItems(renderInfo[id]); } return renderInfo; } @@ -304,22 +304,63 @@ export class SyncedTabsController { return null; } - getTabItems(tabs) { - return tabs?.map(tab => ({ - icon: tab.icon, - title: tab.title, - time: tab.lastUsed * 1000, - url: tab.url, - fxaDeviceId: tab.fxaDeviceId, - primaryL10nId: "firefoxview-tabs-list-tab-button", - primaryL10nArgs: JSON.stringify({ targetURI: tab.url }), - secondaryL10nId: this.contextMenu - ? "fxviewtabrow-options-menu-button" - : undefined, - secondaryL10nArgs: this.contextMenu - ? JSON.stringify({ tabTitle: tab.title }) - : undefined, - })); + /** + * Turn renderInfo into a list of tabs for syncedtabs-tab-list + * + * @param {object} renderInfo + * @param {Array} [renderInfo.tabs] + * tabs to display to the user + * @param {string} [renderInfo.name] + * The name of the device for use when the user hovers over + * the close button for context + * @param {boolean} [renderInfo.canClose] + * Whether the list should support remotely closing tabs + */ + getTabItems({ tabs, name, canClose }) { + return tabs + ?.map(tab => { + let tabItem = { + icon: tab.icon, + title: tab.title, + time: tab.lastUsed * 1000, + url: tab.url, + fxaDeviceId: tab.fxaDeviceId, + primaryL10nId: "firefoxview-tabs-list-tab-button", + primaryL10nArgs: JSON.stringify({ targetURI: tab.url }), + secondaryL10nId: this.contextMenu + ? "fxviewtabrow-options-menu-button" + : undefined, + secondaryL10nArgs: this.contextMenu + ? JSON.stringify({ tabTitle: tab.title }) + : undefined, + }; + // We don't want to show the option to close remotely if the + // device doesn't support it + if (!canClose) { + return tabItem; + } + + // If this item has been requested to be closed, show + // the undo instead until removed from the list + if (tabItem.url === this.lastClosedURL) { + tabItem.tertiaryL10nId = "text-action-undo"; + tabItem.tertiaryActionClass = "undo-button"; + tabItem.tertiaryL10nArgs = null; + tabItem.closeRequested = true; + } else { + // Otherwise default to showing the close/dismiss button + tabItem.tertiaryL10nId = "synced-tabs-context-close-tab-title"; + tabItem.tertiaryL10nArgs = JSON.stringify({ deviceName: name }); + tabItem.tertiaryActionClass = "dismiss-button"; + tabItem.closeRequested = false; + } + return tabItem; + }) + .filter( + item => + !this.isURLQueuedToClose(item.fxaDeviceId, item.url) || + item.url === this.lastClosedURL + ); } updateTabsList(syncedTabs) { diff --git a/browser/components/firefoxview/card-container.mjs b/browser/components/firefoxview/card-container.mjs index 6d96f91edb65..9c14800f90c8 100644 --- a/browser/components/firefoxview/card-container.mjs +++ b/browser/components/firefoxview/card-container.mjs @@ -114,7 +114,9 @@ class CardContainer extends MozLitElement { } updateTabLists() { - let tabLists = this.querySelectorAll("fxview-tab-list, opentabs-tab-list"); + let tabLists = this.querySelectorAll( + "fxview-tab-list, opentabs-tab-list, syncedtabs-tab-list" + ); if (tabLists) { tabLists.forEach(tabList => { tabList.updatesPaused = !this.visible || !this.isExpanded; diff --git a/browser/components/firefoxview/firefoxview.html b/browser/components/firefoxview/firefoxview.html index bdaa41bd7c47..ecff78f06dfe 100644 --- a/browser/components/firefoxview/firefoxview.html +++ b/browser/components/firefoxview/firefoxview.html @@ -14,6 +14,8 @@ + + { + return html` + + `; + }; + + stylesheets() { + return [ + super.stylesheets(), + html``, + ]; + } + + render() { + if (this.searchQuery && !this.tabItems.length) { + return this.emptySearchResultsTemplate(); + } + return html` + ${this.stylesheets()} +
+ ${when( + lazy.virtualListEnabledPref, + () => html` + + `, + () => + html`${this.tabItems.map((tabItem, i) => + this.itemTemplate(tabItem, i) + )}` + )} +
+ + `; + } +} + +customElements.define("syncedtabs-tab-list", SyncedTabsTabList); + +/** + * A tab item that displays favicon, title, url, and time of last access + * + * @property {boolean} canClose - Whether the tab item has the ability to be closed remotely + * @property {boolean} closeRequested - Whether the tab has been requested closed but not removed from the list + * @property {string} fxaDeviceId - The device Id the tab item belongs to, for closing tabs remotely + */ + +export class SyncedTabsTabRow extends FxviewTabRowBase { + constructor() { + super(); + } + + static properties = { + ...FxviewTabRowBase.properties, + canClose: { type: Boolean }, + closeRequested: { type: Boolean }, + fxaDeviceId: { type: String }, + }; + + secondaryButtonTemplate() { + return html`${when( + this.secondaryL10nId && this.secondaryActionHandler, + () => html`` + )}`; + } + + render() { + return html` + ${this.stylesheets()} + + ${this.faviconTemplate()} ${this.titleTemplate()} + ${when( + !this.compact, + () => html`${this.urlTemplate()} ${this.dateTemplate()} + ${this.timeTemplate()}` + )} + + ${this.secondaryButtonTemplate()} ${this.tertiaryButtonTemplate()} + `; + } +} + +customElements.define("syncedtabs-tab-row", SyncedTabsTabRow); diff --git a/browser/components/firefoxview/syncedtabs.mjs b/browser/components/firefoxview/syncedtabs.mjs index 15a24e095bf9..3311c1a3d31a 100644 --- a/browser/components/firefoxview/syncedtabs.mjs +++ b/browser/components/firefoxview/syncedtabs.mjs @@ -22,6 +22,8 @@ import { MAX_TABS_FOR_RECENT_BROWSING, navigateToLink, } from "./helpers.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/firefoxview/syncedtabs-tab-list.mjs"; const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open"; @@ -61,7 +63,7 @@ class SyncedTabsInView extends ViewPage { cardEls: { all: "card-container" }, emptyState: "fxview-empty-state", searchTextbox: "fxview-search-textbox", - tabLists: { all: "fxview-tab-list" }, + tabLists: { all: "syncedtabs-tab-list" }, }; start() { @@ -186,6 +188,18 @@ class SyncedTabsInView extends ViewPage { e.target.querySelector("panel-list").toggle(e.detail.originalEvent); } + onCloseTab(e) { + const { url, fxaDeviceId, tertiaryActionClass } = e.originalTarget; + if (tertiaryActionClass === "dismiss-button") { + // Set new pending close tab + this.controller.requestCloseRemoteTab(fxaDeviceId, url); + } else if (tertiaryActionClass === "undo-button") { + // User wants to undo + this.controller.removePendingTabToClose(fxaDeviceId, url); + } + this.requestUpdate(); + } + panelListTemplate() { return html` @@ -259,19 +273,19 @@ class SyncedTabsInView extends ViewPage { ${deviceName} - ${this.panelListTemplate()} - `; + `; } generateTabList() { diff --git a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js index ba5dd2b5793f..b4d41666b5b2 100644 --- a/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js +++ b/browser/components/firefoxview/tests/browser/browser_syncedtabs_firefoxview.js @@ -475,18 +475,20 @@ add_task(async function search_synced_tabs() { is(syncedTabsComponent.cardEls.length, 2, "There are two device cards."); await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list") && - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls - .length && - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list") && - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list").rowEls - .length, + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") && + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") + .rowEls.length && + syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list") && + syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list") + .rowEls.length, "The tab list has loaded for the first two cards." ); - let deviceOneTabs = - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls; - let deviceTwoTabs = - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list").rowEls; + let deviceOneTabs = syncedTabsComponent.cardEls[0].querySelector( + "syncedtabs-tab-list" + ).rowEls; + let deviceTwoTabs = syncedTabsComponent.cardEls[1].querySelector( + "syncedtabs-tab-list" + ).rowEls; info("Input a search query."); EventUtils.synthesizeMouseAtCenter( @@ -501,19 +503,20 @@ add_task(async function search_synced_tabs() { ); await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list") && - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls - .length, + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") && + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") + .rowEls.length, "The tab list has loaded for the first card." ); await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls - .length === 1, + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") + .rowEls.length === 1, "There is one matching search result for the first device." ); await TestUtils.waitForCondition( - () => !syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list"), + () => + !syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list"), "There are no matching search results for the second device." ); @@ -529,28 +532,30 @@ add_task(async function search_synced_tabs() { ); await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list") && - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls - .length && - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list") && - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list").rowEls - .length, + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") && + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") + .rowEls.length && + syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list") && + syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list") + .rowEls.length, "The tab list has loaded for the first two cards." ); - deviceOneTabs = - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls; - deviceTwoTabs = - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list").rowEls; + deviceOneTabs = syncedTabsComponent.cardEls[0].querySelector( + "syncedtabs-tab-list" + ).rowEls; + deviceTwoTabs = syncedTabsComponent.cardEls[1].querySelector( + "syncedtabs-tab-list" + ).rowEls; await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls - .length === deviceOneTabs.length, + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") + .rowEls.length === deviceOneTabs.length, "The original device's list is restored." ); await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list").rowEls - .length === deviceTwoTabs.length, + syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list") + .rowEls.length === deviceTwoTabs.length, "The new devices's list is restored." ); syncedTabsComponent.searchTextbox.blur(); @@ -564,19 +569,20 @@ add_task(async function search_synced_tabs() { ); await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list") && - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls - .length, + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") && + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") + .rowEls.length, "The tab list has loaded for the first card." ); await TestUtils.waitForCondition(() => { return ( - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls - .length === 1 + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") + .rowEls.length === 1 ); }, "There is one matching search result for the first device."); await TestUtils.waitForCondition( - () => !syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list"), + () => + !syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list"), "There are no matching search results for the second device." ); @@ -598,28 +604,30 @@ add_task(async function search_synced_tabs() { ); await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list") && - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls - .length && - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list") && - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list").rowEls - .length, + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") && + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") + .rowEls.length && + syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list") && + syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list") + .rowEls.length, "The tab list has loaded for the first two cards." ); - deviceOneTabs = - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls; - deviceTwoTabs = - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list").rowEls; + deviceOneTabs = syncedTabsComponent.cardEls[0].querySelector( + "syncedtabs-tab-list" + ).rowEls; + deviceTwoTabs = syncedTabsComponent.cardEls[1].querySelector( + "syncedtabs-tab-list" + ).rowEls; await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[0].querySelector("fxview-tab-list").rowEls - .length === deviceOneTabs.length, + syncedTabsComponent.cardEls[0].querySelector("syncedtabs-tab-list") + .rowEls.length === deviceOneTabs.length, "The original device's list is restored." ); await TestUtils.waitForCondition( () => - syncedTabsComponent.cardEls[1].querySelector("fxview-tab-list").rowEls - .length === deviceTwoTabs.length, + syncedTabsComponent.cardEls[1].querySelector("syncedtabs-tab-list") + .rowEls.length === deviceTwoTabs.length, "The new devices's list is restored." ); }); diff --git a/browser/components/firefoxview/tests/browser/head.js b/browser/components/firefoxview/tests/browser/head.js index 14aeca8961b8..8b8d4efe1af5 100644 --- a/browser/components/firefoxview/tests/browser/head.js +++ b/browser/components/firefoxview/tests/browser/head.js @@ -320,6 +320,11 @@ function setupMocks({ fxaDevices = null, state, syncEnabled = true }) { }), }; }); + + // whatever was passed in was the "found" client + sandbox + .stub(SyncedTabs._internal, "_getClientFxaDeviceId") + .callsFake(clientId => clientId); // This is converting the device list to a client list. // There are two primary differences: // 1. The client list doesn't return the current device. diff --git a/browser/components/firefoxview/viewpage.mjs b/browser/components/firefoxview/viewpage.mjs index 0b42cf856d53..eba7a793ba10 100644 --- a/browser/components/firefoxview/viewpage.mjs +++ b/browser/components/firefoxview/viewpage.mjs @@ -201,7 +201,9 @@ export class ViewPage extends ViewPageContent { let tabLists = []; if (!isOpenTabs) { cards = this.shadowRoot.querySelectorAll("card-container"); - tabLists = this.shadowRoot.querySelectorAll("fxview-tab-list"); + tabLists = this.shadowRoot.querySelectorAll( + "fxview-tab-list, syncedtabs-tab-list" + ); } else { this.viewCards.forEach(viewCard => { if (viewCard.cardEl) {