1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
10 } from "chrome://global/content/vendor/lit.all.mjs";
11 import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
15 MAX_TABS_FOR_RECENT_BROWSING,
16 } from "./helpers.mjs";
17 import { searchTabList } from "./search-helpers.mjs";
18 import { ViewPage, ViewPageContent } from "./viewpage.mjs";
19 // eslint-disable-next-line import/no-unassigned-import
20 import "chrome://browser/content/firefoxview/opentabs-tab-list.mjs";
24 ChromeUtils.defineESModuleGetters(lazy, {
25 BookmarkList: "resource://gre/modules/BookmarkList.sys.mjs",
26 ContextualIdentityService:
27 "resource://gre/modules/ContextualIdentityService.sys.mjs",
28 NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
29 NonPrivateTabs: "resource:///modules/OpenTabs.sys.mjs",
30 getTabsTargetForWindow: "resource:///modules/OpenTabs.sys.mjs",
31 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
34 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
35 return ChromeUtils.importESModule(
36 "resource://gre/modules/FxAccounts.sys.mjs"
37 ).getFxAccountsSingleton();
40 const TOPIC_DEVICESTATE_CHANGED = "firefox-view.devicestate.changed";
41 const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
44 * A collection of open tabs grouped by window.
46 * @property {Array<Window>} windows
47 * A list of windows with the same privateness
48 * @property {string} sortOption
49 * The sorting order of open tabs:
50 * - "recency": Sorted by recent activity. (For recent browsing, this is the only option.)
51 * - "tabStripOrder": Match the order in which they appear on the tab strip.
53 class OpenTabsInView extends ViewPage {
55 ...ViewPage.properties,
56 windows: { type: Array },
57 searchQuery: { type: String },
58 sortOption: { type: String },
61 viewCards: { all: "view-opentabs-card" },
62 optionsContainer: ".open-tabs-options",
63 searchTextbox: "fxview-search-textbox",
66 initialWindowsReady = false;
68 openTabsTarget = null;
72 this._started = false;
74 this.currentWindow = this.getWindow();
75 if (lazy.PrivateBrowsingUtils.isWindowPrivate(this.currentWindow)) {
76 this.openTabsTarget = lazy.getTabsTargetForWindow(this.currentWindow);
78 this.openTabsTarget = lazy.NonPrivateTabs;
80 this.searchQuery = "";
81 this.sortOption = this.recentBrowsing
83 : Services.prefs.getStringPref(
84 "browser.tabs.firefox-view.ui-state.opentabs.sort-option",
94 this.#setupTabChangeListener();
96 // To resolve the race between this component wanting to render all the windows'
97 // tabs, while those windows are still potentially opening, flip this property
98 // once the promise resolves and we'll bail out of rendering until then.
99 this.openTabsTarget.readyWindowsPromise.finally(() => {
100 this.initialWindowsReady = true;
101 this._updateWindowList();
104 for (let card of this.viewCards) {
106 card.viewVisibleCallback?.();
109 if (this.recentBrowsing) {
110 this.recentBrowsingElement.addEventListener(
111 "fxview-search-textbox-query",
116 this.bookmarkList = new lazy.BookmarkList(this.#getAllTabUrls(), () =>
117 this.viewCards.forEach(card => card.requestUpdate())
121 shouldUpdate(changedProperties) {
122 if (!this.initialWindowsReady) {
125 return super.shouldUpdate(changedProperties);
128 disconnectedCallback() {
129 super.disconnectedCallback();
134 if (!this._started) {
137 this._started = false;
140 this.openTabsTarget.removeEventListener("TabChange", this);
141 this.openTabsTarget.removeEventListener("TabRecencyChange", this);
143 for (let card of this.viewCards) {
145 card.viewHiddenCallback?.();
148 if (this.recentBrowsing) {
149 this.recentBrowsingElement.removeEventListener(
150 "fxview-search-textbox-query",
155 this.bookmarkList.removeListeners();
158 viewVisibleCallback() {
162 viewHiddenCallback() {
166 #setupTabChangeListener() {
167 if (this.sortOption === "recency") {
168 this.openTabsTarget.addEventListener("TabRecencyChange", this);
169 this.openTabsTarget.removeEventListener("TabChange", this);
171 this.openTabsTarget.removeEventListener("TabRecencyChange", this);
172 this.openTabsTarget.addEventListener("TabChange", this);
177 return this.openTabsTarget
179 .map(({ linkedBrowser }) => linkedBrowser?.currentURI?.spec)
184 if (this.recentBrowsing) {
185 return this.getRecentBrowsingTemplate();
187 let currentWindowIndex, currentWindowTabs;
189 const otherWindows = [];
190 this.windows.forEach(win => {
191 const tabs = this.openTabsTarget.getTabsForWindow(
193 this.sortOption === "recency"
195 if (win === this.currentWindow) {
196 currentWindowIndex = index++;
197 currentWindowTabs = tabs;
199 otherWindows.push([index++, tabs, win]);
203 const cardClasses = classMap({
204 "height-limited": this.windows.length > 3,
205 "width-limited": this.windows.length > 1,
208 if (this.windows.length <= 1) {
210 } else if (this.windows.length === 2) {
213 cardCount = "three-or-more";
218 href="chrome://browser/content/firefoxview/view-opentabs.css"
222 href="chrome://browser/content/firefoxview/firefoxview.css"
224 <div class="sticky-container bottom-fade">
225 <h2 class="page-header" data-l10n-id="firefoxview-opentabs-header"></h2>
226 <div class="open-tabs-options">
228 <fxview-search-textbox
229 data-l10n-id="firefoxview-search-text-box-opentabs"
230 data-l10n-attrs="placeholder"
231 @fxview-search-textbox-query=${this.onSearchQuery}
232 .size=${this.searchTextboxSize}
233 pageName=${this.recentBrowsing ? "recentbrowsing" : "opentabs"}
234 ></fxview-search-textbox>
236 <div class="open-tabs-sort-wrapper">
237 <div class="open-tabs-sort-option">
241 name="open-tabs-sort-option"
243 ?checked=${this.sortOption === "recency"}
244 @click=${this.onChangeSortOption}
247 for="sort-by-recency"
248 data-l10n-id="firefoxview-sort-open-tabs-by-recency-label"
251 <div class="open-tabs-sort-option">
255 name="open-tabs-sort-option"
256 value="tabStripOrder"
257 ?checked=${this.sortOption === "tabStripOrder"}
258 @click=${this.onChangeSortOption}
262 data-l10n-id="firefoxview-sort-open-tabs-by-order-label"
269 card-count=${cardCount}
270 class="view-opentabs-card-container cards-container"
273 currentWindowIndex && currentWindowTabs,
277 .tabs=${currentWindowTabs}
278 .paused=${this.paused}
279 data-inner-id="${this.currentWindow.windowGlobalChild
281 data-l10n-id="firefoxview-opentabs-current-window-header"
282 data-l10n-args="${JSON.stringify({
283 winID: currentWindowIndex,
285 .searchQuery=${this.searchQuery}
286 .bookmarkList=${this.bookmarkList}
287 ></view-opentabs-card>
292 ([winID, tabs, win]) => html`
296 .paused=${this.paused}
297 data-inner-id="${win.windowGlobalChild.innerWindowId}"
298 data-l10n-id="firefoxview-opentabs-window-header"
299 data-l10n-args="${JSON.stringify({ winID })}"
300 .searchQuery=${this.searchQuery}
301 .bookmarkList=${this.bookmarkList}
302 ></view-opentabs-card>
310 this.searchQuery = e.detail.query;
313 onChangeSortOption(e) {
314 this.sortOption = e.target.value;
315 this.#setupTabChangeListener();
316 if (!this.recentBrowsing) {
317 Services.prefs.setStringPref(
318 "browser.tabs.firefox-view.ui-state.opentabs.sort-option",
325 * Render a template for the 'Recent browsing' page, which shows a shorter list of
326 * open tabs in the current window.
328 * @returns {TemplateResult}
329 * The recent browsing template.
331 getRecentBrowsingTemplate() {
332 const tabs = this.openTabsTarget.getRecentTabs();
333 return html`<view-opentabs-card
335 .recentBrowsing=${true}
336 .paused=${this.paused}
337 .searchQuery=${this.searchQuery}
338 .bookmarkList=${this.bookmarkList}
339 ></view-opentabs-card>`;
342 handleEvent({ detail, type }) {
343 if (this.recentBrowsing && type === "fxview-search-textbox-query") {
344 this.onSearchQuery({ detail });
349 case "TabRecencyChange":
351 windowIds = detail.windowIds;
352 this._updateWindowList();
353 this.bookmarkList.setTrackedUrls(this.#getAllTabUrls());
356 if (this.recentBrowsing) {
359 if (windowIds?.length) {
360 // there were tab changes to one or more windows
361 for (let winId of windowIds) {
362 const cardForWin = this.shadowRoot.querySelector(
363 `view-opentabs-card[data-inner-id="${winId}"]`
365 if (this.searchQuery) {
366 cardForWin?.updateSearchResults();
368 cardForWin?.requestUpdate();
371 let winId = window.windowGlobalChild.innerWindowId;
372 let cardForWin = this.shadowRoot.querySelector(
373 `view-opentabs-card[data-inner-id="${winId}"]`
375 if (this.searchQuery) {
376 cardForWin?.updateSearchResults();
381 async _updateWindowList() {
382 this.windows = this.openTabsTarget.currentWindows;
385 customElements.define("view-opentabs", OpenTabsInView);
388 * A card which displays a list of open tabs for a window.
390 * @property {boolean} showMore
391 * Whether to force all tabs to be shown, regardless of available space.
392 * @property {MozTabbrowserTab[]} tabs
393 * The open tabs to show.
394 * @property {string} title
397 class OpenTabsInViewCard extends ViewPageContent {
398 static properties = {
399 showMore: { type: Boolean },
400 tabs: { type: Array },
401 title: { type: String },
402 recentBrowsing: { type: Boolean },
403 searchQuery: { type: String },
404 searchResults: { type: Array },
405 showAll: { type: Boolean },
406 cumulativeSearches: { type: Number },
407 bookmarkList: { type: Object },
409 static MAX_TABS_FOR_COMPACT_HEIGHT = 7;
413 this.showMore = false;
416 this.recentBrowsing = false;
418 this.searchQuery = "";
419 this.searchResults = null;
420 this.showAll = false;
421 this.cumulativeSearches = 0;
425 cardEl: "card-container",
426 tabContextMenu: "view-opentabs-contextmenu",
427 tabList: "opentabs-tab-list",
431 let { originalEvent } = e.detail;
432 this.tabContextMenu.toggle({
433 triggerNode: e.originalTarget,
439 if (this.recentBrowsing && !this.showAll) {
440 return MAX_TABS_FOR_RECENT_BROWSING;
441 } else if (this.classList.contains("height-limited") && !this.showMore) {
442 return OpenTabsInViewCard.MAX_TABS_FOR_COMPACT_HEIGHT;
447 isShowAllLinkVisible() {
449 this.recentBrowsing &&
451 this.searchResults.length > MAX_TABS_FOR_RECENT_BROWSING &&
456 toggleShowMore(event) {
458 event.type == "click" ||
459 (event.type == "keydown" && event.code == "Enter") ||
460 (event.type == "keydown" && event.code == "Space")
462 event.preventDefault();
463 this.showMore = !this.showMore;
467 enableShowAll(event) {
469 event.type == "click" ||
470 (event.type == "keydown" && event.code == "Enter") ||
471 (event.type == "keydown" && event.code == "Space")
473 event.preventDefault();
474 Glean.firefoxviewNext.searchShowAllShowallbutton.record({
481 onTabListRowClick(event) {
482 // Don't open pinned tab if mute/unmute indicator button selected
484 Array.from(event.explicitOriginalTarget.classList).includes(
485 "fxview-tab-row-pinned-media-button"
490 const tab = event.originalTarget.tabElement;
491 const browserWindow = tab.ownerGlobal;
492 browserWindow.focus();
493 browserWindow.gBrowser.selectedTab = tab;
495 Glean.firefoxviewNext.openTabTabs.record({
496 page: this.recentBrowsing ? "recentbrowsing" : "opentabs",
497 window: this.title || "Window 1 (Current)",
499 if (this.searchQuery) {
501 .getKeyedHistogramById("FIREFOX_VIEW_CUMULATIVE_SEARCHES")
503 this.recentBrowsing ? "recentbrowsing" : "opentabs",
504 this.cumulativeSearches
506 this.cumulativeSearches = 0;
511 const tab = event.originalTarget.tabElement;
512 tab?.ownerGlobal.gBrowser.removeTab(tab);
514 Glean.firefoxviewNext.closeOpenTabTabs.record();
517 viewVisibleCallback() {
518 this.getRootNode().host.toggleVisibilityInCardContainer(true);
521 viewHiddenCallback() {
522 this.getRootNode().host.toggleVisibilityInCardContainer(true);
526 this.getRootNode().host.toggleVisibilityInCardContainer(true);
533 href="chrome://browser/content/firefoxview/firefoxview.css"
536 ?preserveCollapseState=${this.recentBrowsing}
537 shortPageName=${this.recentBrowsing ? "opentabs" : null}
538 ?showViewAll=${this.recentBrowsing}
539 ?removeBlockEndMargin=${!this.recentBrowsing}
546 data-l10n-id="firefoxview-opentabs-header"
548 () => html`<h3 slot="header">${this.title}</h3>`
550 <div class="fxview-tab-list-container" slot="main">
553 ?compactRows=${this.classList.contains("width-limited")}
554 @fxview-tab-list-primary-action=${this.onTabListRowClick}
555 @fxview-tab-list-secondary-action=${this.openContextMenu}
556 @fxview-tab-list-tertiary-action=${this.closeTab}
557 secondaryActionClass="options-button"
558 tertiaryActionClass="dismiss-button"
559 .maxTabsLength=${this.getMaxTabsLength()}
560 .tabItems=${this.searchResults ||
561 getTabListItems(this.tabs, this.recentBrowsing)}
562 .searchQuery=${this.searchQuery}
563 .pinnedTabsGridView=${!this.recentBrowsing}
564 ><view-opentabs-contextmenu slot="menu"></view-opentabs-contextmenu>
571 @click=${this.enableShowAll}
572 @keydown=${this.enableShowAll}
573 data-l10n-id="firefoxview-show-all"
574 ?hidden=${!this.isShowAllLinkVisible()}
581 @click=${this.toggleShowMore}
582 @keydown=${this.toggleShowMore}
583 data-l10n-id="${this.showMore
584 ? "firefoxview-show-less"
585 : "firefoxview-show-more"}"
586 ?hidden=${!this.classList.contains("height-limited") ||
588 OpenTabsInViewCard.MAX_TABS_FOR_COMPACT_HEIGHT}
598 willUpdate(changedProperties) {
599 if (changedProperties.has("searchQuery")) {
600 this.showAll = false;
601 this.cumulativeSearches = this.searchQuery
602 ? this.cumulativeSearches + 1
605 if (changedProperties.has("searchQuery") || changedProperties.has("tabs")) {
606 this.updateSearchResults();
610 updateSearchResults() {
611 this.searchResults = this.searchQuery
612 ? searchTabList(this.searchQuery, getTabListItems(this.tabs))
617 this.updateBookmarkStars();
620 async updateBookmarkStars() {
621 const tabItems = [...this.tabList.tabItems];
622 for (const row of tabItems) {
623 const isBookmark = await this.bookmarkList.isBookmark(row.url);
624 if (isBookmark && !row.indicators.includes("bookmark")) {
625 row.indicators.push("bookmark");
627 if (!isBookmark && row.indicators.includes("bookmark")) {
628 row.indicators = row.indicators.filter(i => i !== "bookmark");
630 row.primaryL10nId = getPrimaryL10nId(
631 this.isRecentBrowsing,
635 this.tabList.tabItems = tabItems;
638 customElements.define("view-opentabs-card", OpenTabsInViewCard);
641 * A context menu of actions available for open tab list items.
643 class OpenTabsContextMenu extends MozLitElement {
644 static properties = {
645 devices: { type: Array },
646 triggerNode: { hasChanged: () => true, type: Object },
650 panelList: "panel-list",
655 this.triggerNode = null;
656 this.boundObserve = (...args) => this.observe(...args);
661 return getLogger("OpenTabsContextMenu");
664 get ownerViewPage() {
665 return this.ownerDocument.querySelector("view-opentabs");
668 connectedCallback() {
669 super.connectedCallback();
670 this.fetchDevicesPromise = this.fetchDevices();
671 Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
672 Services.obs.addObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED);
675 disconnectedCallback() {
676 super.disconnectedCallback();
677 Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED);
678 Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED);
681 observe(_subject, topic, _data) {
683 topic == TOPIC_DEVICELIST_UPDATED ||
684 topic == TOPIC_DEVICESTATE_CHANGED
686 this.fetchDevicesPromise = this.fetchDevices();
690 async fetchDevices() {
691 const currentWindow = this.ownerViewPage.getWindow();
692 if (currentWindow?.gSync) {
694 await lazy.fxAccounts.device.refreshDeviceList();
696 this.logger.warn("Could not refresh the FxA device list", e);
698 this.devices = currentWindow.gSync.getSendTabTargets();
702 async toggle({ triggerNode, originalEvent }) {
703 if (this.panelList?.open) {
704 // the menu will close so avoid all the other work to update its contents
705 this.panelList.toggle(originalEvent);
708 this.triggerNode = triggerNode;
709 await this.fetchDevicesPromise;
710 await this.getUpdateComplete();
711 this.panelList.toggle(originalEvent);
715 placeLinkOnClipboard(this.triggerNode.title, this.triggerNode.url);
716 this.ownerViewPage.recordContextMenuTelemetry("copy-link", e);
720 const tab = this.triggerNode.tabElement;
721 tab?.ownerGlobal.gBrowser.removeTab(tab);
722 this.ownerViewPage.recordContextMenuTelemetry("close-tab", e);
726 const tab = this.triggerNode.tabElement;
727 tab?.ownerGlobal.gBrowser.pinTab(tab);
728 this.ownerViewPage.recordContextMenuTelemetry("pin-tab", e);
732 const tab = this.triggerNode.tabElement;
733 tab?.ownerGlobal.gBrowser.unpinTab(tab);
734 this.ownerViewPage.recordContextMenuTelemetry("unpin-tab", e);
738 const tab = this.triggerNode.tabElement;
739 tab.toggleMuteAudio();
740 this.ownerViewPage.recordContextMenuTelemetry(
742 this.triggerNode.indicators.includes("muted") ? "unmute" : "mute"
749 const tab = this.triggerNode.tabElement;
750 tab?.ownerGlobal.gBrowser.moveTabsToStart(tab);
751 this.ownerViewPage.recordContextMenuTelemetry("move-tab-start", e);
755 const tab = this.triggerNode.tabElement;
756 tab?.ownerGlobal.gBrowser.moveTabsToEnd(tab);
757 this.ownerViewPage.recordContextMenuTelemetry("move-tab-end", e);
760 moveTabsToWindow(e) {
761 const tab = this.triggerNode.tabElement;
762 tab?.ownerGlobal.gBrowser.replaceTabsWithWindow(tab);
763 this.ownerViewPage.recordContextMenuTelemetry("move-tab-window", e);
767 const tab = this.triggerNode?.tabElement;
771 const browserWindow = tab.ownerGlobal;
772 const tabs = browserWindow?.gBrowser.visibleTabs || [];
773 const position = tabs.indexOf(tab);
776 <panel-list slot="submenu" id="move-tab-menu">
779 @click=${this.moveTabsToStart}
780 data-l10n-id="fxviewtabrow-move-tab-start"
781 data-l10n-attrs="accesskey"
784 ${position < tabs.length - 1
786 @click=${this.moveTabsToEnd}
787 data-l10n-id="fxviewtabrow-move-tab-end"
788 data-l10n-attrs="accesskey"
792 @click=${this.moveTabsToWindow}
793 data-l10n-id="fxviewtabrow-move-tab-window"
794 data-l10n-attrs="accesskey"
800 async sendTabToDevice(e) {
801 let deviceId = e.target.getAttribute("device-id");
802 let device = this.devices.find(dev => dev.id == deviceId);
803 const viewPage = this.ownerViewPage;
804 viewPage.recordContextMenuTelemetry("send-tab-device", e);
806 if (device && this.triggerNode) {
809 .gSync.sendTabToDevice(
810 this.triggerNode.url,
812 this.triggerNode.title
818 return html` <panel-list slot="submenu" id="send-tab-menu">
819 ${this.devices.map(device => {
821 <panel-item @click=${this.sendTabToDevice} device-id=${device.id}
822 >${device.name}</panel-item
830 const tab = this.triggerNode?.tabElement;
838 href="chrome://browser/content/firefoxview/firefoxview.css"
840 <panel-list data-tab-type="opentabs">
842 data-l10n-id="fxviewtabrow-move-tab"
843 data-l10n-attrs="accesskey"
844 submenu="move-tab-menu"
845 >${this.moveMenuTemplate()}</panel-item
848 data-l10n-id=${tab.pinned
849 ? "fxviewtabrow-unpin-tab"
850 : "fxviewtabrow-pin-tab"}
851 data-l10n-attrs="accesskey"
852 @click=${tab.pinned ? this.unpinTab : this.pinTab}
855 data-l10n-id=${tab.hasAttribute("muted")
856 ? "fxviewtabrow-unmute-tab"
857 : "fxviewtabrow-mute-tab"}
858 data-l10n-attrs="accesskey"
859 @click=${this.toggleAudio}
863 data-l10n-id="fxviewtabrow-copy-link"
864 data-l10n-attrs="accesskey"
865 @click=${this.copyLink}
867 ${this.devices.length >= 1
869 data-l10n-id="fxviewtabrow-send-tab"
870 data-l10n-attrs="accesskey"
871 submenu="send-tab-menu"
872 >${this.sendTabTemplate()}</panel-item
879 customElements.define("view-opentabs-contextmenu", OpenTabsContextMenu);
882 * Checks if a given tab is within a container (contextual identity)
884 * @param {MozTabbrowserTab[]} tab
885 * Tab to fetch container info on.
886 * @returns {object[]}
889 function getContainerObj(tab) {
890 let userContextId = tab.getAttribute("usercontextid");
891 let containerObj = null;
894 lazy.ContextualIdentityService.getPublicIdentityFromId(userContextId);
900 * Gets an array of tab indicators (if any) when normalizing for fxview-tab-list
902 * @param {MozTabbrowserTab[]} tab
903 * Tab to fetch container info on.
905 * Array of named tab indicators
907 function getIndicatorsForTab(tab) {
908 const url = tab.linkedBrowser?.currentURI?.spec || "";
909 let tabIndicators = [];
912 (tab.hasAttribute("attention") || tab.hasAttribute("titlechanged"))) ||
913 (!tab.pinned && tab.hasAttribute("attention"));
916 tabIndicators.push("pinned");
918 if (getContainerObj(tab)) {
919 tabIndicators.push("container");
922 tabIndicators.push("attention");
924 if (tab.hasAttribute("soundplaying") && !tab.hasAttribute("muted")) {
925 tabIndicators.push("soundplaying");
927 if (tab.hasAttribute("muted")) {
928 tabIndicators.push("muted");
930 if (checkIfPinnedNewTab(url)) {
931 tabIndicators.push("pinnedOnNewTab");
934 return tabIndicators;
937 * Gets the primary l10n id for a tab when normalizing for fxview-tab-list
939 * @param {boolean} isRecentBrowsing
940 * Whether the tabs are going to be displayed on the Recent Browsing page or not
941 * @param {Array[]} tabIndicators
942 * Array of tab indicators for the given tab
946 function getPrimaryL10nId(isRecentBrowsing, tabIndicators) {
947 let indicatorL10nId = null;
948 if (!isRecentBrowsing) {
950 tabIndicators?.includes("pinned") &&
951 tabIndicators?.includes("bookmark")
953 indicatorL10nId = "firefoxview-opentabs-bookmarked-pinned-tab";
954 } else if (tabIndicators?.includes("pinned")) {
955 indicatorL10nId = "firefoxview-opentabs-pinned-tab";
956 } else if (tabIndicators?.includes("bookmark")) {
957 indicatorL10nId = "firefoxview-opentabs-bookmarked-tab";
960 return indicatorL10nId;
964 * Gets the primary l10n args for a tab when normalizing for fxview-tab-list
966 * @param {MozTabbrowserTab[]} tab
967 * Tab to fetch container info on.
968 * @param {boolean} isRecentBrowsing
969 * Whether the tabs are going to be displayed on the Recent Browsing page or not
970 * @param {string} url
971 * URL for the given tab
975 function getPrimaryL10nArgs(tab, isRecentBrowsing, url) {
976 return JSON.stringify({ tabTitle: tab.label, url });
980 * Check if a given url is pinned on the new tab page
982 * @param {string} url
985 * is tabbed pinned on new tab page
987 function checkIfPinnedNewTab(url) {
988 return url && lazy.NewTabUtils.pinnedLinks.isPinned({ url });
992 * Convert a list of tabs into the format expected by the fxview-tab-list
995 * @param {MozTabbrowserTab[]} tabs
997 * @param {boolean} isRecentBrowsing
998 * Whether the tabs are going to be displayed on the Recent Browsing page or not
999 * @returns {object[]}
1000 * Formatted objects.
1002 function getTabListItems(tabs, isRecentBrowsing) {
1003 let filtered = tabs?.filter(tab => !tab.closing && !tab.hidden);
1005 return filtered.map(tab => {
1006 let tabIndicators = getIndicatorsForTab(tab);
1007 let containerObj = getContainerObj(tab);
1008 const url = tab?.linkedBrowser?.currentURI?.spec || "";
1011 indicators: tabIndicators,
1012 icon: tab.getAttribute("image"),
1013 primaryL10nId: getPrimaryL10nId(isRecentBrowsing, tabIndicators),
1014 primaryL10nArgs: getPrimaryL10nArgs(tab, isRecentBrowsing, url),
1016 isRecentBrowsing || (!isRecentBrowsing && !tab.pinned)
1017 ? "fxviewtabrow-options-menu-button"
1020 isRecentBrowsing || (!isRecentBrowsing && !tab.pinned)
1021 ? JSON.stringify({ tabTitle: tab.label })
1024 isRecentBrowsing || (!isRecentBrowsing && !tab.pinned)
1025 ? "fxviewtabrow-close-tab-button"
1028 isRecentBrowsing || (!isRecentBrowsing && !tab.pinned)
1029 ? JSON.stringify({ tabTitle: tab.label })
1032 time: tab.lastSeenActive,