Bug 1911626: Add closing tabs remotely to sidebar r=markh,lina,fluent-reviewers,desktop-theme-reviewers,fxview-reviewers,sync-reviewers,sidebar-reviewers,dao,jsudiaman

Differential Revision: https://phabricator.services.mozilla.com/D218902
This commit is contained in:
Sammy Khamis 2024-09-04 00:48:53 +00:00
parent 49bd0ece94
commit 253e97cde2
9 changed files with 191 additions and 9 deletions

View File

@ -6,6 +6,8 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
SyncedTabsManagement: "resource://services-sync/SyncedTabs.sys.mjs",
COMMAND_CLOSETAB: "resource://gre/modules/FxAccountsCommon.sys.mjs",
});
import { SyncedTabsErrorHandler } from "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs";
@ -69,6 +71,11 @@ export class SyncedTabsController {
this.observe = this.observe.bind(this);
this.host = host;
this.host.addController(this);
// Track tabs requested close per device but not-yet-sent,
// it'll be in the form of {fxaDeviceId: Set(urls)}
this._pendingCloseTabs = new Map();
// The last closed URL, for undo purposes
this.lastClosedURL = null;
}
hostConnected() {
@ -134,6 +141,10 @@ export class SyncedTabsController {
await this.updateStates(errorState);
}
if (topic == SYNCED_TABS_CHANGED) {
// Usually this means we performed a sync, so clear the
// "in-queue" things as those most likely got flushed
this._pendingCloseTabs = new Map();
this.lastClosedURL = null;
await this.getSyncedTabData();
}
}
@ -232,6 +243,7 @@ export class SyncedTabsController {
renderInfo[tab.client] = {
name: tab.device,
deviceType: tab.deviceType,
canClose: !!tab.availableCommands[lazy.COMMAND_CLOSETAB],
tabs: [],
};
}
@ -294,6 +306,7 @@ export class SyncedTabsController {
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
@ -330,4 +343,32 @@ export class SyncedTabsController {
this.updateTabsList(tabs);
}
// Wrappers and helpful methods for SyncedTabManagement
// so FxView and Sidebar don't need to import
requestCloseRemoteTab(fxaDeviceId, url) {
if (!this._pendingCloseTabs.has(fxaDeviceId)) {
this._pendingCloseTabs.set(fxaDeviceId, new Set());
}
this._pendingCloseTabs.get(fxaDeviceId).add(url);
this.lastClosedURL = url;
lazy.SyncedTabsManagement.enqueueTabToClose(fxaDeviceId, url);
}
removePendingTabToClose(fxaDeviceId, url) {
const urls = this._pendingCloseTabs.get(fxaDeviceId);
if (urls) {
urls.delete(url);
if (!urls.size) {
this._pendingCloseTabs.delete(fxaDeviceId);
}
}
this.lastClosedURL = null;
lazy.SyncedTabsManagement.removePendingTabToClose(fxaDeviceId, url);
}
isURLQueuedToClose(fxaDeviceId, url) {
const urls = this._pendingCloseTabs.get(fxaDeviceId);
return urls && urls.has(url);
}
}

View File

@ -125,3 +125,8 @@
.fxview-tab-row-button.options-button::part(button) {
background-image: url("chrome://global/skin/icons/more.svg");
}
.fxview-tab-row-button.undo-button::part(button) {
font-size: var(--font-size-small);
font-weight: 400;
}

View File

@ -15,6 +15,7 @@
<link rel="localization" href="browser/sidebar.ftl" />
<link rel="localization" href="browser/firefoxView.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<link rel="localization" href="toolkit/global/textActions.ftl" />
<link rel="stylesheet" href="chrome://global/skin/global.css" />
<link
rel="stylesheet"
@ -32,6 +33,10 @@
type="module"
src="chrome://browser/content/firefoxview/fxview-search-textbox.mjs"
></script>
<script
type="module"
src="chrome://browser/content/sidebar/sidebar-tab-list.mjs"
></script>
<script
type="module"
src="chrome://browser/content/firefoxview/fxview-tab-list.mjs"

View File

@ -29,6 +29,7 @@ class SyncedTabsInSidebar extends SidebarPage {
constructor() {
super();
this.onSearchQuery = this.onSearchQuery.bind(this);
this.onSecondaryAction = this.onSecondaryAction.bind(this);
}
connectedCallback() {
@ -45,7 +46,7 @@ class SyncedTabsInSidebar extends SidebarPage {
}
handleContextMenuEvent(e) {
this.triggerNode = this.findTriggerNode(e, "fxview-tab-row");
this.triggerNode = this.findTriggerNode(e, "sidebar-tab-row");
if (!this.triggerNode) {
e.preventDefault();
}
@ -65,6 +66,18 @@ class SyncedTabsInSidebar extends SidebarPage {
}
}
onSecondaryAction(e) {
const { url, fxaDeviceId, secondaryActionClass } = e.originalTarget;
if (secondaryActionClass === "dismiss-button") {
// Set new pending close tab
this.controller.requestCloseRemoteTab(fxaDeviceId, url);
} else if (secondaryActionClass === "undo-button") {
this.controller.removePendingTabToClose(fxaDeviceId, url);
}
this.requestUpdate();
}
/**
* The template shown when the list of synced devices is currently
* unavailable.
@ -130,12 +143,13 @@ class SyncedTabsInSidebar extends SidebarPage {
icon
class=${deviceType}
>
<fxview-tab-list
<sidebar-tab-list
compactRows
.tabItems=${ifDefined(tabItems)}
.tabItems=${tabItems}
.updatesPaused=${false}
.searchQuery=${this.controller.searchQuery}
@fxview-tab-list-primary-action=${navigateToLink}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
/>
</moz-card>`;
}
@ -185,9 +199,13 @@ class SyncedTabsInSidebar extends SidebarPage {
*/
deviceListTemplate() {
return Object.values(this.controller.getRenderInfo()).map(
({ name: deviceName, deviceType, tabItems, tabs }) => {
({ name: deviceName, deviceType, tabItems, canClose, tabs }) => {
if (tabItems.length) {
return this.deviceTemplate(deviceName, deviceType, tabItems);
return this.deviceTemplate(
deviceName,
deviceType,
this.getTabItems(tabItems, deviceName, canClose)
);
} else if (tabs.length) {
return this.noSearchResultsTemplate(deviceName, deviceType);
}
@ -196,6 +214,40 @@ class SyncedTabsInSidebar extends SidebarPage {
);
}
getTabItems(items, deviceName, canClose) {
return items
.map(item => {
if (!canClose) {
return item;
}
// Default show the close/dismiss button
let secondaryActionClass = "dismiss-button";
let secondaryL10nId = "synced-tabs-context-close-tab-title";
let secondaryL10nArgs = JSON.stringify({ deviceName });
// If this item has been requested to be closed, show
// the undo instead
if (item.url === this.controller.lastClosedURL) {
secondaryActionClass = "undo-button";
secondaryL10nId = "text-action-undo";
secondaryL10nArgs = null;
}
return {
...item,
secondaryActionClass,
secondaryL10nId,
secondaryL10nArgs,
};
})
.filter(
item =>
!this.controller.isURLQueuedToClose(item.fxaDeviceId, item.url) ||
item.url === this.controller.lastClosedURL
);
}
render() {
const messageCard = this.controller.getMessageCard();
return html`

View File

@ -45,13 +45,15 @@ export class SidebarTabList extends FxviewTabListBase {
.closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
compact
.currentActiveElementId=${this.currentActiveElementId}
.fxaDeviceId=${ifDefined(tabItem.fxaDeviceId)}
.favicon=${tabItem.icon}
.hasPopup=${this.hasPopup}
.primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
.primaryL10nId=${tabItem.primaryL10nId}
role="listitem"
.searchQuery=${ifDefined(this.searchQuery)}
.secondaryActionClass=${this.secondaryActionClass}
.secondaryActionClass=${this.secondaryActionClass ??
tabItem.secondaryActionClass}
.secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
.secondaryL10nId=${tabItem.secondaryL10nId}
.sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
@ -94,7 +96,7 @@ export class SidebarTabRow extends FxviewTabRowBase {
[this.secondaryActionClass]: this.secondaryActionClass,
})}
data-l10n-args=${ifDefined(this.secondaryL10nArgs)}
data-l10n-id=${this.secondaryL10nId}
data-l10n-id=${ifDefined(this.secondaryL10nId)}
id="fxview-tab-row-secondary-button"
type="icon ghost"
@click=${this.secondaryActionHandler}

View File

@ -7,5 +7,6 @@
:host(:hover) & {
visibility: visible;
justify-self: end;
}
}

View File

@ -30,6 +30,8 @@ const tabClients = [
icon: "https://sinonjs.org/assets/images/favicon.png",
lastUsed: 1655391592, // Thu Jun 16 2022 14:59:52 GMT+0000
client: 1,
fxaDeviceId: 1,
availableCommands: ["https://identity.mozilla.com/cmd/close-uri/v1"],
},
{
device: "My desktop",
@ -40,6 +42,8 @@ const tabClients = [
icon: "https://www.mozilla.org/media/img/favicons/mozilla/favicon.d25d81d39065.ico",
lastUsed: 1655730486, // Mon Jun 20 2022 13:08:06 GMT+0000
client: 1,
fxaDeviceId: 1,
availableCommands: ["https://identity.mozilla.com/cmd/close-uri/v1"],
},
],
},
@ -59,6 +63,8 @@ const tabClients = [
icon: "page-icon:https://www.theguardian.com/",
lastUsed: 1655291890, // Wed Jun 15 2022 11:18:10 GMT+0000
client: 2,
fxaDeviceId: 2,
availableCommands: ["https://identity.mozilla.com/cmd/close-uri/v1"],
},
{
device: "My iphone",
@ -69,6 +75,8 @@ const tabClients = [
icon: "page-icon:https://www.thetimes.co.uk/",
lastUsed: 1655727485, // Mon Jun 20 2022 12:18:05 GMT+0000
client: 2,
fxaDeviceId: 2,
availableCommands: ["https://identity.mozilla.com/cmd/close-uri/v1"],
},
],
},
@ -93,18 +101,57 @@ add_task(async function test_tabs() {
const card = component.cards[i];
Assert.equal(card.heading, client.name, "Device name is correct.");
const rows = await TestUtils.waitForCondition(() => {
const { rowEls } = card.querySelector("fxview-tab-list");
const { rowEls } = card.querySelector("sidebar-tab-list");
return rowEls.length === client.tabs.length && rowEls;
}, "Device has the correct number of tabs.");
for (const [j, row] of rows.entries()) {
const tabData = client.tabs[j];
Assert.equal(row.title, tabData.title, `Tab ${j + 1} has correct title.`);
Assert.equal(row.url, tabData.url, `Tab ${j + 1} has correct URL.`);
// Simulate hovering over the row to reveal the dismiss button
EventUtils.synthesizeMouseAtCenter(
row.mainEl,
{},
SidebarController.browser.contentWindow
);
await BrowserTestUtils.waitForCondition(
() => row.querySelector(".dismiss-button") !== null,
"Hovered over the row"
);
// Check the presence of the dismiss button
const dismissButton = row.querySelector(".dismiss-button");
Assert.ok(dismissButton, `Dismiss button is present on tab ${j + 1}.`);
// Simulate clicking the dismiss button
EventUtils.synthesizeMouseAtCenter(
dismissButton,
{},
SidebarController.browser.contentWindow
);
await TestUtils.waitForCondition(() => {
const undoButton = row.querySelector(".undo-button");
return undoButton && undoButton.style.display !== "none";
}, `Undo button is shown after dismissing tab ${j + 1}.`);
// Simulate clicking the undo button
const undoButton = row.querySelector(".undo-button");
EventUtils.synthesizeMouseAtCenter(
undoButton,
{},
SidebarController.browser.contentWindow
);
await TestUtils.waitForCondition(() => {
return (
row.querySelector(".dismiss-button") &&
!row.querySelector(".undo-button")
);
}, `Dismiss button is restored after undoing tab ${j + 1}.`);
}
}
info("Copy the first link.");
const tabList = component.cards[0].querySelector("fxview-tab-list");
const tabList = component.cards[0].querySelector("sidebar-tab-list");
const menuItem = document.getElementById(
"sidebar-synced-tabs-context-copy-link"
);

View File

@ -102,3 +102,12 @@ sidebar-menu-history-header =
.heading = History
sidebar-menu-syncedtabs-header =
.heading = Tabs from other devices
## Context for closing synced tabs when hovering over the items
# Context for hovering over the close tab button that will
# send a push to the device to close said tab
# Variables
# $deviceName - the name of the device the user is closing a tab for
synced-tabs-context-close-tab-title =
.title = Close tab on { $deviceName }

View File

@ -21,6 +21,12 @@ ChromeUtils.defineLazyGetter(lazy, "weaveXPCService", function () {
).wrappedJSObject;
});
ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
return ChromeUtils.importESModule(
"resource://gre/modules/FxAccounts.sys.mjs"
).getFxAccountsSingleton();
});
// from MDN...
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@ -116,9 +122,23 @@ let SyncedTabsInternal = {
if (extraParams.removeDeviceDupes) {
client.tabs = this._filterRecentTabsDupes(client.tabs);
}
// 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 =
lazy.fxAccounts.device.recentDeviceList &&
lazy.fxAccounts.device.recentDeviceList.find(
d =>
d.id ===
lazy.Weave.Service.clientsEngine.getClientFxaDeviceId(client.id)
);
for (let tab of client.tabs) {
tab.device = client.name;
tab.deviceType = client.clientType;
// Surface broadcasted commmands for things like close remote tab
tab.fxaDeviceId = device.id;
tab.availableCommands = device.availableCommands;
}
tabs = [...tabs, ...client.tabs.reverse()];
}