Bug 1920852: Add closing tabs remotely to Firefox View r=lina,fxview-reviewers,jsudiaman

Differential Revision: https://phabricator.services.mozilla.com/D223448
This commit is contained in:
Sammy Khamis 2024-10-01 21:08:02 +00:00
parent 7dc01ce4ff
commit 3c7cbd61fb
11 changed files with 370 additions and 76 deletions

View File

@ -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<object>} [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) {

View File

@ -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;

View File

@ -14,6 +14,8 @@
<title data-l10n-id="firefoxview-page-title"></title>
<link rel="localization" href="branding/brand.ftl" />
<link rel="localization" href="browser/firefoxView.ftl" />
<link rel="localization" href="browser/sidebar.ftl" />
<link rel="localization" href="toolkit/global/textActions.ftl" />
<link rel="localization" href="toolkit/branding/brandings.ftl" />
<link rel="localization" href="browser/migrationWizard.ftl" />
<link

View File

@ -125,6 +125,10 @@
.fxview-tab-row-button.dismiss-button::part(button) {
background-image: url("chrome://global/skin/icons/close.svg");
-moz-context-properties: fill;
fill: currentColor;
background-repeat: no-repeat;
background-position: center;
}
.fxview-tab-row-button.options-button::part(button) {
@ -135,3 +139,8 @@
font-size: var(--font-size-small);
font-weight: 400;
}
.fxview-tab-row-button.dismiss-button,
.fxview-tab-row-button.undo-button {
justify-self: end;
}

View File

@ -27,6 +27,8 @@ browser.jar:
content/browser/firefoxview/opentabs-tab-list.css
content/browser/firefoxview/opentabs-tab-list.mjs
content/browser/firefoxview/opentabs-tab-row.css
content/browser/firefoxview/syncedtabs-tab-list.css
content/browser/firefoxview/syncedtabs-tab-list.mjs
content/browser/firefoxview/recentlyclosed.mjs
content/browser/firefoxview/viewpage.mjs
content/browser/firefoxview/history-empty.svg (content/history-empty.svg)

View File

@ -0,0 +1,14 @@
/* 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/. */
virtual-list {
display: grid;
grid-column: span 9;
grid-template-columns: subgrid;
.top-padding,
.bottom-padding {
grid-column: span 9;
}
}

View File

@ -0,0 +1,195 @@
/* 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 {
classMap,
html,
ifDefined,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
import {
FxviewTabListBase,
FxviewTabRowBase,
} from "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://global/content/elements/moz-button.mjs";
const lazy = {};
let XPCOMUtils;
XPCOMUtils = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
).XPCOMUtils;
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"virtualListEnabledPref",
"browser.firefox-view.virtual-list.enabled"
);
/**
* A list of synced tabs that are clickable and able to be remotely closed
*/
export class SyncedTabsTabList extends FxviewTabListBase {
constructor() {
super();
}
static queries = {
...FxviewTabListBase.queries,
rowEls: {
all: "syncedtabs-tab-row",
},
};
itemTemplate = (tabItem, i) => {
return html`
<syncedtabs-tab-row
?active=${i == this.activeIndex}
.canClose=${ifDefined(tabItem.canClose)}
.closeRequested=${ifDefined(tabItem.closeRequested)}
?compact=${this.compactRows}
.currentActiveElementId=${this.currentActiveElementId}
.favicon=${tabItem.icon}
.fxaDeviceId=${tabItem.fxaDeviceId}
.primaryL10nId=${tabItem.primaryL10nId}
.primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
.secondaryL10nId=${tabItem.secondaryL10nId}
.secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
.tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)}
.tertiaryL10nArgs=${ifDefined(tabItem.tertiaryL10nArgs)}
.secondaryActionClass=${this.secondaryActionClass}
.tertiaryActionClass=${ifDefined(tabItem.tertiaryActionClass)}
.sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
.sourceWindowId=${ifDefined(tabItem.sourceWindowId)}
.closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
role="listitem"
.tabElement=${ifDefined(tabItem.tabElement)}
.title=${tabItem.title}
.url=${tabItem.url}
.searchQuery=${ifDefined(this.searchQuery)}
.hasPopup=${this.hasPopup}
></fxview-tab-row>
`;
};
stylesheets() {
return [
super.stylesheets(),
html`<link
rel="stylesheet"
href="chrome://browser/content/firefoxview/syncedtabs-tab-list.css"
/>`,
];
}
render() {
if (this.searchQuery && !this.tabItems.length) {
return this.emptySearchResultsTemplate();
}
return html`
${this.stylesheets()}
<div
id="fxview-tab-list"
class="fxview-tab-list"
data-l10n-id="firefoxview-tabs"
role="list"
@keydown=${this.handleFocusElementInRow}
>
${when(
lazy.virtualListEnabledPref,
() => html`
<virtual-list
.activeIndex=${this.activeIndex}
.items=${this.tabItems}
.template=${this.itemTemplate}
></virtual-list>
`,
() =>
html`${this.tabItems.map((tabItem, i) =>
this.itemTemplate(tabItem, i)
)}`
)}
</div>
<slot name="menu"></slot>
`;
}
}
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`<moz-button
type="icon ghost"
class=${classMap({
"fxview-tab-row-button": true,
[this.secondaryActionClass]: this.secondaryActionClass,
})}
?disabled=${this.closeRequested}
id="fxview-tab-row-secondary-button"
data-l10n-id=${this.secondaryL10nId}
data-l10n-args=${ifDefined(this.secondaryL10nArgs)}
aria-haspopup=${ifDefined(this.hasPopup)}
@click=${this.secondaryActionHandler}
tabindex="${this.active &&
this.currentActiveElementId === "fxview-tab-row-secondary-button"
? "0"
: "-1"}"
></moz-button>`
)}`;
}
render() {
return html`
${this.stylesheets()}
<a
href=${ifDefined(this.url)}
class="fxview-tab-row-main"
id="fxview-tab-row-main"
disabled=${this.closeRequested}
tabindex=${this.active &&
this.currentActiveElementId === "fxview-tab-row-main"
? "0"
: "-1"}
data-l10n-id=${ifDefined(this.primaryL10nId)}
data-l10n-args=${ifDefined(this.primaryL10nArgs)}
@click=${this.primaryActionHandler}
@keydown=${this.primaryActionHandler}
title=${!this.primaryL10nId ? this.url : null}
>
${this.faviconTemplate()} ${this.titleTemplate()}
${when(
!this.compact,
() => html`${this.urlTemplate()} ${this.dateTemplate()}
${this.timeTemplate()}`
)}
</a>
${this.secondaryButtonTemplate()} ${this.tertiaryButtonTemplate()}
`;
}
}
customElements.define("syncedtabs-tab-row", SyncedTabsTabRow);

View File

@ -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`
<panel-list slot="menu" data-tab-type="syncedtabs">
@ -259,19 +273,19 @@ class SyncedTabsInView extends ViewPage {
<span class="icon ${deviceType}" role="presentation"></span>
${deviceName}
</h3>
<fxview-tab-list
<syncedtabs-tab-list
slot="main"
secondaryActionClass="options-button"
hasPopup="menu"
.hasPopup=${"menu"}
.tabItems=${ifDefined(tabItems)}
.searchQuery=${this.controller.searchQuery}
maxTabsLength=${this.showAll ? -1 : this.maxTabsLength}
.maxTabsLength=${this.showAll ? -1 : this.maxTabsLength}
@fxview-tab-list-primary-action=${this.onOpenLink}
@fxview-tab-list-secondary-action=${this.onContextMenu}
@fxview-tab-list-tertiary-action=${this.onCloseTab}
secondaryActionClass="options-button"
>
${this.panelListTemplate()}
</fxview-tab-list>`;
</syncedtabs-tab-list>`;
}
generateTabList() {

View File

@ -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."
);
});

View File

@ -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.

View File

@ -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) {