From d8bbc7858622b6d9c278469aab701ca0b609cddf Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 15 May 2024 05:35:49 +0200 Subject: Merging upstream version 126.0. Signed-off-by: Daniel Baumann --- .../components/firefoxview/HistoryController.mjs | 188 ++++++ browser/components/firefoxview/OpenTabs.sys.mjs | 55 +- .../firefoxview/SyncedTabsController.sys.mjs | 333 ++++++++++ browser/components/firefoxview/card-container.css | 6 +- browser/components/firefoxview/card-container.mjs | 2 +- .../firefox-view-tabs-setup-manager.sys.mjs | 17 +- browser/components/firefoxview/firefoxview.css | 19 +- browser/components/firefoxview/firefoxview.html | 12 +- browser/components/firefoxview/firefoxview.mjs | 16 + .../components/firefoxview/fxview-empty-state.css | 2 +- browser/components/firefoxview/fxview-tab-list.css | 22 +- browser/components/firefoxview/fxview-tab-list.mjs | 689 +++++++-------------- browser/components/firefoxview/fxview-tab-row.css | 178 +----- browser/components/firefoxview/helpers.mjs | 17 + browser/components/firefoxview/history.css | 13 +- browser/components/firefoxview/history.mjs | 224 ++----- browser/components/firefoxview/jar.mn | 4 + .../components/firefoxview/opentabs-tab-list.css | 32 + .../components/firefoxview/opentabs-tab-list.mjs | 593 ++++++++++++++++++ .../components/firefoxview/opentabs-tab-row.css | 119 ++++ browser/components/firefoxview/opentabs.mjs | 42 +- browser/components/firefoxview/recentlyclosed.mjs | 16 +- browser/components/firefoxview/syncedtabs.mjs | 387 +++--------- .../firefoxview/tests/browser/browser.toml | 8 +- .../browser_firefoxview_dragDrop_pinned_tab.js | 102 +++ .../tests/browser/browser_firefoxview_paused.js | 99 --- .../browser_firefoxview_search_telemetry.js | 5 +- .../tests/browser/browser_firefoxview_tab.js | 6 +- .../tests/browser/browser_history_firefoxview.js | 15 +- .../tests/browser/browser_opentabs_cards.js | 10 +- .../tests/browser/browser_opentabs_firefoxview.js | 4 +- .../tests/browser/browser_opentabs_recency.js | 350 +++++++---- .../browser/browser_opentabs_tab_indicators.js | 10 +- .../browser/browser_recentlyclosed_firefoxview.js | 12 + .../browser_syncedtabs_errors_firefoxview.js | 272 +++++++- .../browser/browser_syncedtabs_firefoxview.js | 7 +- .../browser_tab_list_keyboard_navigation.js | 4 +- .../tests/browser/browser_tab_on_close_warning.js | 2 +- .../tests/chrome/test_fxview_tab_list.html | 5 - 39 files changed, 2451 insertions(+), 1446 deletions(-) create mode 100644 browser/components/firefoxview/HistoryController.mjs create mode 100644 browser/components/firefoxview/SyncedTabsController.sys.mjs create mode 100644 browser/components/firefoxview/opentabs-tab-list.css create mode 100644 browser/components/firefoxview/opentabs-tab-list.mjs create mode 100644 browser/components/firefoxview/opentabs-tab-row.css create mode 100644 browser/components/firefoxview/tests/browser/browser_firefoxview_dragDrop_pinned_tab.js (limited to 'browser/components/firefoxview') diff --git a/browser/components/firefoxview/HistoryController.mjs b/browser/components/firefoxview/HistoryController.mjs new file mode 100644 index 0000000000..d2bda5cec2 --- /dev/null +++ b/browser/components/firefoxview/HistoryController.mjs @@ -0,0 +1,188 @@ +/* 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, { + FirefoxViewPlacesQuery: + "resource:///modules/firefox-view-places-query.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", +}); + +let XPCOMUtils = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +).XPCOMUtils; + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "maxRowsPref", + "browser.firefox-view.max-history-rows", + -1 +); + +const HISTORY_MAP_L10N_IDS = { + sidebar: { + "history-date-today": "sidebar-history-date-today", + "history-date-yesterday": "sidebar-history-date-yesterday", + "history-date-this-month": "sidebar-history-date-this-month", + "history-date-prev-month": "sidebar-history-date-prev-month", + }, + firefoxview: { + "history-date-today": "firefoxview-history-date-today", + "history-date-yesterday": "firefoxview-history-date-yesterday", + "history-date-this-month": "firefoxview-history-date-this-month", + "history-date-prev-month": "firefoxview-history-date-prev-month", + }, +}; + +export class HistoryController { + host; + allHistoryItems; + historyMapByDate; + historyMapBySite; + searchQuery; + searchResults; + sortOption; + + constructor(host, options) { + this.allHistoryItems = new Map(); + this.historyMapByDate = []; + this.historyMapBySite = []; + this.placesQuery = new lazy.FirefoxViewPlacesQuery(); + this.searchQuery = ""; + this.searchResults = null; + this.sortOption = "date"; + this.searchResultsLimit = options?.searchResultsLimit || 300; + this.component = HISTORY_MAP_L10N_IDS?.[options?.component] + ? options?.component + : "firefoxview"; + this.host = host; + + host.addController(this); + } + + async hostConnected() { + this.placesQuery.observeHistory(data => this.updateAllHistoryItems(data)); + await this.updateHistoryData(); + this.createHistoryMaps(); + } + + hostDisconnected() { + this.placesQuery.close(); + } + + deleteFromHistory() { + lazy.PlacesUtils.history.remove(this.host.triggerNode.url); + } + + async onSearchQuery(e) { + this.searchQuery = e.detail.query; + await this.updateSearchResults(); + this.host.requestUpdate(); + } + + async onChangeSortOption(e) { + this.sortOption = e.target.value; + await this.updateHistoryData(); + await this.updateSearchResults(); + this.host.requestUpdate(); + } + + async updateHistoryData() { + this.allHistoryItems = await this.placesQuery.getHistory({ + daysOld: 60, + limit: lazy.maxRowsPref, + sortBy: this.sortOption, + }); + } + + async updateAllHistoryItems(allHistoryItems) { + if (allHistoryItems) { + this.allHistoryItems = allHistoryItems; + } else { + await this.updateHistoryData(); + } + this.resetHistoryMaps(); + this.host.requestUpdate(); + await this.updateSearchResults(); + } + + async updateSearchResults() { + if (this.searchQuery) { + try { + this.searchResults = await this.placesQuery.searchHistory( + this.searchQuery, + this.searchResultsLimit + ); + } catch (e) { + // Connection interrupted, ignore. + } + } else { + this.searchResults = null; + } + } + + resetHistoryMaps() { + this.historyMapByDate = []; + this.historyMapBySite = []; + } + + createHistoryMaps() { + if (!this.historyMapByDate.length) { + const { + visitsFromToday, + visitsFromYesterday, + visitsByDay, + visitsByMonth, + } = this.placesQuery; + + // Add visits from today and yesterday. + if (visitsFromToday.length) { + this.historyMapByDate.push({ + l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-today"], + items: visitsFromToday, + }); + } + if (visitsFromYesterday.length) { + this.historyMapByDate.push({ + l10nId: + HISTORY_MAP_L10N_IDS[this.component]["history-date-yesterday"], + items: visitsFromYesterday, + }); + } + + // Add visits from this month, grouped by day. + visitsByDay.forEach(visits => { + this.historyMapByDate.push({ + l10nId: + HISTORY_MAP_L10N_IDS[this.component]["history-date-this-month"], + items: visits, + }); + }); + + // Add visits from previous months, grouped by month. + visitsByMonth.forEach(visits => { + this.historyMapByDate.push({ + l10nId: + HISTORY_MAP_L10N_IDS[this.component]["history-date-prev-month"], + items: visits, + }); + }); + } else if ( + this.sortOption === "site" && + !this.historyMapBySite.length && + this.component === "firefoxview" + ) { + this.historyMapBySite = Array.from( + this.allHistoryItems.entries(), + ([domain, items]) => ({ + domain, + items, + l10nId: domain ? null : "firefoxview-history-site-localhost", + }) + ).sort((a, b) => a.domain.localeCompare(b.domain)); + } + this.host.requestUpdate(); + } +} diff --git a/browser/components/firefoxview/OpenTabs.sys.mjs b/browser/components/firefoxview/OpenTabs.sys.mjs index 0771bf9e65..6d67ca44cc 100644 --- a/browser/components/firefoxview/OpenTabs.sys.mjs +++ b/browser/components/firefoxview/OpenTabs.sys.mjs @@ -33,6 +33,7 @@ const TAB_CHANGE_EVENTS = Object.freeze([ ]); const TAB_RECENCY_CHANGE_EVENTS = Object.freeze([ "activate", + "sizemodechange", "TabAttrModified", "TabClose", "TabOpen", @@ -75,6 +76,10 @@ class OpenTabsTarget extends EventTarget { TabChange: new Set(), TabRecencyChange: new Set(), }; + #sourceEventsByType = { + TabChange: new Set(), + TabRecencyChange: new Set(), + }; #dispatchChangesTask; #started = false; #watchedWindows = new Set(); @@ -143,7 +148,7 @@ class OpenTabsTarget extends EventTarget { windowList.map(win => win.delayedStartupPromise) ).then(() => { // re-filter the list as properties might have changed in the interim - return windowList.filter(win => this.includeWindowFilter); + return windowList.filter(() => this.includeWindowFilter); }); } @@ -223,6 +228,9 @@ class OpenTabsTarget extends EventTarget { for (let changedWindows of Object.values(this.#changedWindowsByType)) { changedWindows.clear(); } + for (let sourceEvents of Object.values(this.#sourceEventsByType)) { + sourceEvents.clear(); + } this.#watchedWindows.clear(); this.#dispatchChangesTask?.disarm(); } @@ -245,9 +253,16 @@ class OpenTabsTarget extends EventTarget { tabContainer.addEventListener("TabUnpinned", this); tabContainer.addEventListener("TabSelect", this); win.addEventListener("activate", this); + win.addEventListener("sizemodechange", this); - this.#scheduleEventDispatch("TabChange", {}); - this.#scheduleEventDispatch("TabRecencyChange", {}); + this.#scheduleEventDispatch("TabChange", { + sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: "watchWindow", + }); + this.#scheduleEventDispatch("TabRecencyChange", { + sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: "watchWindow", + }); } /** @@ -270,9 +285,16 @@ class OpenTabsTarget extends EventTarget { tabContainer.removeEventListener("TabSelect", this); tabContainer.removeEventListener("TabUnpinned", this); win.removeEventListener("activate", this); + win.removeEventListener("sizemodechange", this); - this.#scheduleEventDispatch("TabChange", {}); - this.#scheduleEventDispatch("TabRecencyChange", {}); + this.#scheduleEventDispatch("TabChange", { + sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: "unwatchWindow", + }); + this.#scheduleEventDispatch("TabRecencyChange", { + sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: "unwatchWindow", + }); } } @@ -281,11 +303,12 @@ class OpenTabsTarget extends EventTarget { * Repeated calls within approx 16ms will be consolidated * into one event dispatch. */ - #scheduleEventDispatch(eventType, { sourceWindowId } = {}) { + #scheduleEventDispatch(eventType, { sourceWindowId, sourceEvent } = {}) { if (!this.haveListenersForEvent(eventType)) { return; } + this.#sourceEventsByType[eventType].add(sourceEvent); this.#changedWindowsByType[eventType].add(sourceWindowId); // Queue up an event dispatch - we use a deferred task to make this less noisy by // consolidating multiple change events into one. @@ -302,16 +325,18 @@ class OpenTabsTarget extends EventTarget { for (let [eventType, changedWindowIds] of Object.entries( this.#changedWindowsByType )) { + let sourceEvents = this.#sourceEventsByType[eventType]; if (this.haveListenersForEvent(eventType) && changedWindowIds.size) { - this.dispatchEvent( - new CustomEvent(eventType, { - detail: { - windowIds: [...changedWindowIds], - }, - }) - ); + let changeEvent = new CustomEvent(eventType, { + detail: { + windowIds: [...changedWindowIds], + sourceEvents: [...sourceEvents], + }, + }); + this.dispatchEvent(changeEvent); changedWindowIds.clear(); } + sourceEvents?.clear(); } } @@ -362,11 +387,13 @@ class OpenTabsTarget extends EventTarget { if (TAB_RECENCY_CHANGE_EVENTS.includes(type)) { this.#scheduleEventDispatch("TabRecencyChange", { sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: type, }); } if (TAB_CHANGE_EVENTS.includes(type)) { this.#scheduleEventDispatch("TabChange", { sourceWindowId: win.windowGlobalChild.innerWindowId, + sourceEvent: type, }); } } @@ -377,7 +404,7 @@ const gExclusiveWindows = new (class { constructor() { Services.obs.addObserver(this, "domwindowclosed"); } - observe(subject, topic, data) { + observe(subject) { let win = subject; let winTarget = this.perWindowInstances.get(win); if (winTarget) { diff --git a/browser/components/firefoxview/SyncedTabsController.sys.mjs b/browser/components/firefoxview/SyncedTabsController.sys.mjs new file mode 100644 index 0000000000..6ab8249bfe --- /dev/null +++ b/browser/components/firefoxview/SyncedTabsController.sys.mjs @@ -0,0 +1,333 @@ +/* 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, { + ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", + SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", +}); + +import { SyncedTabsErrorHandler } from "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs"; +import { TabsSetupFlowManager } from "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs"; +import { searchTabList } from "chrome://browser/content/firefoxview/helpers.mjs"; + +const SYNCED_TABS_CHANGED = "services.sync.tabs.changed"; +const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed"; + +/** + * The controller for synced tabs components. + * + * @implements {ReactiveController} + */ +export class SyncedTabsController { + /** + * @type {boolean} + */ + contextMenu; + currentSetupStateIndex = -1; + currentSyncedTabs = []; + devices = []; + /** + * The current error state as determined by `SyncedTabsErrorHandler`. + * + * @type {number} + */ + errorState = null; + /** + * Component associated with this controller. + * + * @type {ReactiveControllerHost} + */ + host; + /** + * @type {Function} + */ + pairDeviceCallback; + searchQuery = ""; + /** + * @type {Function} + */ + signupCallback; + + /** + * Construct a new SyncedTabsController. + * + * @param {ReactiveControllerHost} host + * @param {object} options + * @param {boolean} [options.contextMenu] + * Whether synced tab items have a secondary context menu. + * @param {Function} [options.pairDeviceCallback] + * The function to call when the pair device window is opened. + * @param {Function} [options.signupCallback] + * The function to call when the signup window is opened. + */ + constructor(host, { contextMenu, pairDeviceCallback, signupCallback } = {}) { + this.contextMenu = contextMenu; + this.pairDeviceCallback = pairDeviceCallback; + this.signupCallback = signupCallback; + this.observe = this.observe.bind(this); + this.host = host; + this.host.addController(this); + } + + hostConnected() { + this.host.addEventListener("click", this); + } + + hostDisconnected() { + this.host.removeEventListener("click", this); + } + + addSyncObservers() { + Services.obs.addObserver(this.observe, SYNCED_TABS_CHANGED); + Services.obs.addObserver(this.observe, TOPIC_SETUPSTATE_CHANGED); + } + + removeSyncObservers() { + Services.obs.removeObserver(this.observe, SYNCED_TABS_CHANGED); + Services.obs.removeObserver(this.observe, TOPIC_SETUPSTATE_CHANGED); + } + + 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); + this.signupCallback?.(); + break; + } + case "add-device": { + TabsSetupFlowManager.openFxAPairDevice(event.target.ownerGlobal); + this.pairDeviceCallback?.(); + 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; + } + } + } + } + + async observe(_, topic, errorState) { + if (topic == TOPIC_SETUPSTATE_CHANGED) { + await this.updateStates(errorState); + } + if (topic == SYNCED_TABS_CHANGED) { + await this.getSyncedTabData(); + } + } + + async updateStates(errorState) { + let stateIndex = TabsSetupFlowManager.uiStateIndex; + errorState = errorState || SyncedTabsErrorHandler.getErrorType(); + + if (stateIndex == 4 && this.currentSetupStateIndex !== stateIndex) { + // trigger an initial request for the synced tabs list + await this.getSyncedTabData(); + } + + this.currentSetupStateIndex = stateIndex; + this.errorState = errorState; + this.host.requestUpdate(); + } + + 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", + descriptionLink: { + name: "url", + url: "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", + buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton", + }, + loading: { + header: "firefoxview-syncedtabs-loading-header", + description: "firefoxview-syncedtabs-loading-description", + }, + }; + + #getMessageCardForState({ error = false, action, errorState }) { + errorState = errorState || this.errorState; + let header, + description, + descriptionLink, + buttonLabel, + 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; + descriptionLink = this.actionMappings[action].descriptionLink; + mainImageUrl = + "chrome://browser/content/firefoxview/synced-tabs-error.svg"; + descriptionArray = [description]; + } + return { + action, + buttonLabel, + descriptionArray, + descriptionLink, + error, + header, + headerIconUrl, + mainImageUrl, + }; + } + + getRenderInfo() { + let renderInfo = {}; + for (let tab of this.currentSyncedTabs) { + if (!(tab.client in renderInfo)) { + renderInfo[tab.client] = { + name: tab.device, + deviceType: tab.deviceType, + tabs: [], + }; + } + renderInfo[tab.client].tabs.push(tab); + } + + // Add devices without tabs + for (let device of this.devices) { + if (!(device.id in renderInfo)) { + renderInfo[device.id] = { + name: device.name, + deviceType: device.clientType, + tabs: [], + }; + } + } + + for (let id in renderInfo) { + renderInfo[id].tabItems = this.searchQuery + ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs)) + : this.getTabItems(renderInfo[id].tabs); + } + return renderInfo; + } + + getMessageCard() { + switch (this.currentSetupStateIndex) { + case 0 /* error-state */: + if (this.errorState) { + return this.#getMessageCardForState({ error: true }); + } + return this.#getMessageCardForState({ action: "loading" }); + 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 + return this.#getMessageCardForState({ + error: true, + errorState: "signed-out", + }); + } + return this.#getMessageCardForState({ action: "sign-in" }); + case 2 /* connect-secondary-device*/: + return this.#getMessageCardForState({ action: "add-device" }); + case 3 /* disabled-tab-sync */: + return this.#getMessageCardForState({ action: "sync-tabs-disabled" }); + case 4 /* synced-tabs-loaded*/: + // There seems to be an edge case where sync says everything worked + // fine but we have no devices. + if (!this.devices.length) { + return this.#getMessageCardForState({ action: "add-device" }); + } + } + return null; + } + + getTabItems(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: this.contextMenu + ? "fxviewtabrow-options-menu-button" + : undefined, + secondaryL10nArgs: this.contextMenu + ? JSON.stringify({ tabTitle: tab.title }) + : undefined, + })); + } + + updateTabsList(syncedTabs) { + if (!syncedTabs.length) { + this.currentSyncedTabs = syncedTabs; + } + + const tabsToRender = syncedTabs; + + // Return early if new tabs are the same as previous ones + if (lazy.ObjectUtils.deepEqual(tabsToRender, this.currentSyncedTabs)) { + return; + } + + this.currentSyncedTabs = tabsToRender; + this.host.requestUpdate(); + } + + async getSyncedTabData() { + this.devices = await lazy.SyncedTabs.getTabClients(); + let tabs = await lazy.SyncedTabs.createRecentTabsList(this.devices, 50, { + removeAllDupes: false, + removeDeviceDupes: true, + }); + + this.updateTabsList(tabs); + } +} diff --git a/browser/components/firefoxview/card-container.css b/browser/components/firefoxview/card-container.css index 953437bec1..0c6a81899b 100644 --- a/browser/components/firefoxview/card-container.css +++ b/browser/components/firefoxview/card-container.css @@ -14,9 +14,9 @@ } } -@media (prefers-contrast) { +@media (forced-colors) or (prefers-contrast) { .card-container { - border: 1px solid CanvasText; + border: 1px solid var(--fxview-border); } } @@ -83,7 +83,7 @@ background-color: var(--fxview-element-background-hover); } -@media (prefers-contrast) { +@media (forced-colors) { .chevron-icon { border: 1px solid ButtonText; color: ButtonText; diff --git a/browser/components/firefoxview/card-container.mjs b/browser/components/firefoxview/card-container.mjs index b58f42204a..1755d97555 100644 --- a/browser/components/firefoxview/card-container.mjs +++ b/browser/components/firefoxview/card-container.mjs @@ -118,7 +118,7 @@ class CardContainer extends MozLitElement { } updateTabLists() { - let tabLists = this.querySelectorAll("fxview-tab-list"); + let tabLists = this.querySelectorAll("fxview-tab-list, opentabs-tab-list"); if (tabLists) { tabLists.forEach(tabList => { tabList.updatesPaused = !this.visible || !this.isExpanded; diff --git a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs index 4c43eea1b6..e1c999d89c 100644 --- a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs +++ b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs @@ -591,12 +591,6 @@ export const TabsSetupFlowManager = new (class { ); this.didFxaTabOpen = true; openTabInWindow(window, url, true); - Services.telemetry.recordEvent( - "firefoxview_next", - "fxa_continue", - "sync", - null - ); } async openFxAPairDevice(window) { @@ -605,18 +599,9 @@ export const TabsSetupFlowManager = new (class { }); this.didFxaTabOpen = true; openTabInWindow(window, url, true); - Services.telemetry.recordEvent( - "firefoxview_next", - "fxa_mobile", - "sync", - null, - { - has_devices: this.secondaryDeviceConnected.toString(), - } - ); } - syncOpenTabs(containerElem) { + syncOpenTabs() { // Flip the pref on. // The observer should trigger re-evaluating state and advance to next step Services.prefs.setBoolPref(SYNC_TABS_PREF, true); diff --git a/browser/components/firefoxview/firefoxview.css b/browser/components/firefoxview/firefoxview.css index 6811ca54c4..a91c90c39e 100644 --- a/browser/components/firefoxview/firefoxview.css +++ b/browser/components/firefoxview/firefoxview.css @@ -31,6 +31,17 @@ --newtab-background-color: #F9F9FB; --fxview-card-header-font-weight: 500; + + /* Make the attention dot color match the browser UI on Linux, and on HCM + * with a lightweight theme. */ + &[lwtheme] { + --attention-dot-color: light-dark(#2ac3a2, #54ffbd); + } + @media (-moz-platform: linux) { + &:not([lwtheme]) { + --attention-dot-color: AccentColor; + } + } } @media (prefers-color-scheme: dark) { @@ -47,7 +58,7 @@ } } -@media (prefers-contrast) { +@media (forced-colors) { :root { --fxview-element-background-hover: ButtonText; --fxview-element-background-active: ButtonText; @@ -59,6 +70,12 @@ } } +@media (prefers-contrast) { + :root { + --fxview-border: var(--border-color); + } +} + @media (max-width: 52rem) { :root { --fxview-sidebar-width: 82px; diff --git a/browser/components/firefoxview/firefoxview.html b/browser/components/firefoxview/firefoxview.html index 6fa0f59a8f..5bffb5a1d8 100644 --- a/browser/components/firefoxview/firefoxview.html +++ b/browser/components/firefoxview/firefoxview.html @@ -72,6 +72,7 @@ >
- +
@@ -104,7 +108,11 @@ name="recentlyclosed" type="page" > - + diff --git a/browser/components/firefoxview/firefoxview.mjs b/browser/components/firefoxview/firefoxview.mjs index 3e61482cc0..e31536bc8b 100644 --- a/browser/components/firefoxview/firefoxview.mjs +++ b/browser/components/firefoxview/firefoxview.mjs @@ -80,6 +80,16 @@ async function updateSearchKeyboardShortcut() { searchKeyboardShortcut = key.toLocaleLowerCase(); } +function updateSyncVisibility() { + const syncEnabled = Services.prefs.getBoolPref( + "identity.fxaccounts.enabled", + false + ); + for (const el of document.querySelectorAll(".sync-ui-item")) { + el.hidden = !syncEnabled; + } +} + window.addEventListener("DOMContentLoaded", async () => { recordEnteredTelemetry(); @@ -106,6 +116,7 @@ window.addEventListener("DOMContentLoaded", async () => { onViewsDeckViewChange(); await updateSearchTextboxSize(); await updateSearchKeyboardShortcut(); + updateSyncVisibility(); if (Cu.isInAutomation) { Services.obs.notifyObservers(null, "firefoxview-entered"); @@ -150,12 +161,17 @@ window.addEventListener( document.body.textContent = ""; topChromeWindow.removeEventListener("command", onCommand); Services.obs.removeObserver(onLocalesChanged, "intl:app-locales-changed"); + Services.prefs.removeObserver( + "identity.fxaccounts.enabled", + updateSyncVisibility + ); }, { once: true } ); topChromeWindow.addEventListener("command", onCommand); Services.obs.addObserver(onLocalesChanged, "intl:app-locales-changed"); +Services.prefs.addObserver("identity.fxaccounts.enabled", updateSyncVisibility); function onCommand(e) { if (document.hidden || !e.target.closest("#contentAreaContextMenu")) { diff --git a/browser/components/firefoxview/fxview-empty-state.css b/browser/components/firefoxview/fxview-empty-state.css index 80b4099e6a..8c0d08c1f8 100644 --- a/browser/components/firefoxview/fxview-empty-state.css +++ b/browser/components/firefoxview/fxview-empty-state.css @@ -93,7 +93,7 @@ img.greyscale { filter: grayscale(100%); - @media not (prefers-contrast) { + @media not (forced-colors) { opacity: 0.5; } } diff --git a/browser/components/firefoxview/fxview-tab-list.css b/browser/components/firefoxview/fxview-tab-list.css index 5a4bff023a..f0881d8ce8 100644 --- a/browser/components/firefoxview/fxview-tab-list.css +++ b/browser/components/firefoxview/fxview-tab-list.css @@ -9,35 +9,21 @@ .fxview-tab-list { display: grid; - grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content; + grid-template-columns: min-content 3fr 2fr 1fr 1fr min-content; gap: var(--space-xsmall); - &.pinned { - display: flex; - flex-wrap: wrap; - - > virtual-list { - display: block; - } - - > fxview-tab-row { - display: block; - margin-block-end: var(--space-xsmall); - } - } - :host([compactRows]) & { - grid-template-columns: min-content 1fr min-content min-content min-content; + grid-template-columns: min-content 1fr min-content min-content; } } virtual-list { display: grid; - grid-column: span 9; + grid-column: span 7; grid-template-columns: subgrid; .top-padding, .bottom-padding { - grid-column: span 9; + grid-column: span 7; } } diff --git a/browser/components/firefoxview/fxview-tab-list.mjs b/browser/components/firefoxview/fxview-tab-list.mjs index 978ab79724..57181e3bea 100644 --- a/browser/components/firefoxview/fxview-tab-list.mjs +++ b/browser/components/firefoxview/fxview-tab-list.mjs @@ -12,6 +12,8 @@ import { } from "chrome://global/content/vendor/lit.all.mjs"; import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; import { escapeRegExp } from "./helpers.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; const NOW_THRESHOLD_MS = 91000; const FXVIEW_ROW_HEIGHT_PX = 32; @@ -45,13 +47,13 @@ if (!window.IS_STORYBOOK) { * @property {string} dateTimeFormat - Expected format for date and/or time * @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required * @property {number} maxTabsLength - The max number of tabs for the list - * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view * @property {Array} tabItems - Items to show in the tab list * @property {string} searchQuery - The query string to highlight, if provided. + * @property {string} searchInProgress - Whether a search has been initiated. * @property {string} secondaryActionClass - The class used to style the secondary action element * @property {string} tertiaryActionClass - The class used to style the tertiary action element */ -export default class FxviewTabList extends MozLitElement { +export class FxviewTabListBase extends MozLitElement { constructor() { super(); window.MozXULElement.insertFTLIfNeeded("toolkit/branding/brandings.ftl"); @@ -62,10 +64,8 @@ export default class FxviewTabList extends MozLitElement { this.dateTimeFormat = "relative"; this.maxTabsLength = 25; this.tabItems = []; - this.pinnedTabs = []; - this.pinnedTabsGridView = false; - this.unpinnedTabs = []; this.compactRows = false; + this.searchInProgress = false; this.updatesPaused = true; this.#register(); } @@ -77,16 +77,18 @@ export default class FxviewTabList extends MozLitElement { dateTimeFormat: { type: String }, hasPopup: { type: String }, maxTabsLength: { type: Number }, - pinnedTabsGridView: { type: Boolean }, tabItems: { type: Array }, updatesPaused: { type: Boolean }, searchQuery: { type: String }, + searchInProgress: { type: Boolean }, secondaryActionClass: { type: String }, tertiaryActionClass: { type: String }, }; static queries = { - rowEls: { all: "fxview-tab-row" }, + rowEls: { + all: "fxview-tab-row", + }, rootVirtualListEl: "virtual-list", }; @@ -108,20 +110,7 @@ export default class FxviewTabList extends MozLitElement { } } - // Move pinned tabs to the beginning of the list - if (this.pinnedTabsGridView) { - // Can set maxTabsLength to -1 to have no max - this.unpinnedTabs = this.tabItems.filter( - tab => !tab.indicators?.includes("pinned") - ); - this.pinnedTabs = this.tabItems.filter(tab => - tab.indicators?.includes("pinned") - ); - if (this.maxTabsLength > 0) { - this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength); - } - this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs]; - } else if (this.maxTabsLength > 0) { + if (this.maxTabsLength > 0) { this.tabItems = this.tabItems.slice(0, this.maxTabsLength); } } @@ -148,7 +137,7 @@ export default class FxviewTabList extends MozLitElement { "timeMsPref", "browser.tabs.firefox-view.updateTimeMs", NOW_THRESHOLD_MS, - (prefName, oldVal, newVal) => { + () => { this.clearIntervalTimer(); if (!this.isConnected) { return; @@ -197,93 +186,32 @@ export default class FxviewTabList extends MozLitElement { if (e.code == "ArrowUp") { // Focus either the link or button of the previous row based on this.currentActiveElementId e.preventDefault(); - if ( - (this.pinnedTabsGridView && - this.activeIndex >= this.pinnedTabs.length) || - !this.pinnedTabsGridView - ) { - this.focusPrevRow(); - } + this.focusPrevRow(); } else if (e.code == "ArrowDown") { // Focus either the link or button of the next row based on this.currentActiveElementId e.preventDefault(); - if ( - this.pinnedTabsGridView && - this.activeIndex < this.pinnedTabs.length - ) { - this.focusIndex(this.pinnedTabs.length); - } else { - this.focusNextRow(); - } + this.focusNextRow(); } else if (e.code == "ArrowRight") { // Focus either the link or the button in the current row and // set this.currentActiveElementId to that element's ID e.preventDefault(); if (document.dir == "rtl") { - this.moveFocusLeft(fxviewTabRow); + fxviewTabRow.moveFocusLeft(); } else { - this.moveFocusRight(fxviewTabRow); + fxviewTabRow.moveFocusRight(); } } else if (e.code == "ArrowLeft") { // Focus either the link or the button in the current row and // set this.currentActiveElementId to that element's ID e.preventDefault(); if (document.dir == "rtl") { - this.moveFocusRight(fxviewTabRow); + fxviewTabRow.moveFocusRight(); } else { - this.moveFocusLeft(fxviewTabRow); + fxviewTabRow.moveFocusLeft(); } } } - moveFocusRight(fxviewTabRow) { - if ( - this.pinnedTabsGridView && - fxviewTabRow.indicators?.includes("pinned") - ) { - this.focusNextRow(); - } else if ( - (fxviewTabRow.indicators?.includes("soundplaying") || - fxviewTabRow.indicators?.includes("muted")) && - this.currentActiveElementId === "fxview-tab-row-main" - ) { - this.currentActiveElementId = fxviewTabRow.focusMediaButton(); - } else if ( - this.currentActiveElementId === "fxview-tab-row-media-button" || - this.currentActiveElementId === "fxview-tab-row-main" - ) { - this.currentActiveElementId = fxviewTabRow.focusSecondaryButton(); - } else if ( - fxviewTabRow.tertiaryButtonEl && - this.currentActiveElementId === "fxview-tab-row-secondary-button" - ) { - this.currentActiveElementId = fxviewTabRow.focusTertiaryButton(); - } - } - - moveFocusLeft(fxviewTabRow) { - if ( - this.pinnedTabsGridView && - (fxviewTabRow.indicators?.includes("pinned") || - (this.currentActiveElementId === "fxview-tab-row-main" && - this.activeIndex === this.pinnedTabs.length)) - ) { - this.focusPrevRow(); - } else if ( - this.currentActiveElementId === "fxview-tab-row-tertiary-button" - ) { - this.currentActiveElementId = fxviewTabRow.focusSecondaryButton(); - } else if ( - (fxviewTabRow.indicators?.includes("soundplaying") || - fxviewTabRow.indicators?.includes("muted")) && - this.currentActiveElementId === "fxview-tab-row-secondary-button" - ) { - this.currentActiveElementId = fxviewTabRow.focusMediaButton(); - } else { - this.currentActiveElementId = fxviewTabRow.focusLink(); - } - } - focusPrevRow() { this.focusIndex(this.activeIndex - 1); } @@ -294,18 +222,12 @@ export default class FxviewTabList extends MozLitElement { async focusIndex(index) { // Focus link or button of item - if ( - ((this.pinnedTabsGridView && index > this.pinnedTabs.length) || - !this.pinnedTabsGridView) && - lazy.virtualListEnabledPref - ) { - let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length); + if (lazy.virtualListEnabledPref) { + let row = this.rootVirtualListEl.getItem(index); if (!row) { return; } - let subList = this.rootVirtualListEl.getSubListForItem( - index - this.pinnedTabs.length - ); + let subList = this.rootVirtualListEl.getSubListForItem(index); if (!subList) { return; } @@ -347,27 +269,15 @@ export default class FxviewTabList extends MozLitElement { time = tabItem.time || tabItem.closedAt; } } + return html` `; }; + stylesheets() { + return html``; + } + render() { - if (this.searchQuery && this.tabItems.length === 0) { - return this.#emptySearchResultsTemplate(); + if ( + this.searchQuery && + this.tabItems.length === 0 && + !this.searchInProgress + ) { + return this.emptySearchResultsTemplate(); } return html` - - ${when( - this.pinnedTabsGridView && this.pinnedTabs.length, - () => html` -
- ${this.pinnedTabs.map((tabItem, i) => - this.itemTemplate(tabItem, i) - )} -
- ` - )} + ${this.stylesheets()}
html` - ` - )} - ${when( - !lazy.virtualListEnabledPref, - () => html` - ${this.tabItems.map((tabItem, i) => this.itemTemplate(tabItem, i))} - ` + `, + () => + html`${this.tabItems.map((tabItem, i) => + this.itemTemplate(tabItem, i) + )}` )}
`; } - #emptySearchResultsTemplate() { + emptySearchResultsTemplate() { return html` `; } } -customElements.define("fxview-tab-list", FxviewTabList); +customElements.define("fxview-tab-list", FxviewTabListBase); /** * A tab item that displays favicon, title, url, and time of last access * * @property {boolean} active - Should current item have focus on keydown * @property {boolean} compact - Whether to hide the URL and date/time for this tab. - * @property {object} containerObj - Info about an open tab's container if within one * @property {string} currentActiveElementId - ID of currently focused element within each tab item * @property {string} dateTimeFormat - Expected format for date and/or time * @property {string} hasPopup - The aria-haspopup attribute for the secondary action, if required - * @property {string} indicators - An array of tab indicators if any are present * @property {number} closedId - The tab ID for when the tab item was closed. * @property {number} sourceClosedId - The closedId of the closed window its from if applicable * @property {number} sourceWindowId - The sessionstore id of the window its from if applicable * @property {string} favicon - The favicon for the tab item. - * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view * @property {string} primaryL10nId - The l10n id used for the primary action element * @property {string} primaryL10nArgs - The l10n args used for the primary action element * @property {string} secondaryL10nId - The l10n id used for the secondary action button @@ -487,23 +382,14 @@ customElements.define("fxview-tab-list", FxviewTabList); * @property {number} timeMsPref - The frequency in milliseconds of updates to relative time * @property {string} searchQuery - The query string to highlight, if provided. */ -export class FxviewTabRow extends MozLitElement { - constructor() { - super(); - this.active = false; - this.currentActiveElementId = "fxview-tab-row-main"; - } - +export class FxviewTabRowBase extends MozLitElement { static properties = { active: { type: Boolean }, compact: { type: Boolean }, - containerObj: { type: Object }, currentActiveElementId: { type: String }, dateTimeFormat: { type: String }, favicon: { type: String }, hasPopup: { type: String }, - indicators: { type: Array }, - pinnedTabsGridView: { type: Boolean }, primaryL10nId: { type: String }, primaryL10nArgs: { type: String }, secondaryL10nId: { type: String }, @@ -523,12 +409,16 @@ export class FxviewTabRow extends MozLitElement { searchQuery: { type: String }, }; + constructor() { + super(); + this.active = false; + this.currentActiveElementId = "fxview-tab-row-main"; + } + static queries = { mainEl: "#fxview-tab-row-main", secondaryButtonEl: "#fxview-tab-row-secondary-button:not([hidden])", tertiaryButtonEl: "#fxview-tab-row-tertiary-button", - mediaButtonEl: "#fxview-tab-row-media-button", - pinnedTabButtonEl: "button#fxview-tab-row-main", }; get currentFocusable() { @@ -539,50 +429,45 @@ export class FxviewTabRow extends MozLitElement { return focusItem; } - connectedCallback() { - super.connectedCallback(); - this.addEventListener("keydown", this.handleKeydown); - } - - disconnectedCallback() { - super.disconnectedCallback(); - this.removeEventListener("keydown", this.handleKeydown); - } - - handleKeydown(e) { - if ( - this.active && - this.pinnedTabsGridView && - this.indicators?.includes("pinned") && - e.key === "m" && - e.ctrlKey - ) { - this.muteOrUnmuteTab(); - } - } - focus() { this.currentFocusable.focus(); } focusSecondaryButton() { + let tabList = this.getRootNode().host; this.secondaryButtonEl.focus(); - return this.secondaryButtonEl.id; + tabList.currentActiveElementId = this.secondaryButtonEl.id; } focusTertiaryButton() { + let tabList = this.getRootNode().host; this.tertiaryButtonEl.focus(); - return this.tertiaryButtonEl.id; - } - - focusMediaButton() { - this.mediaButtonEl.focus(); - return this.mediaButtonEl.id; + tabList.currentActiveElementId = this.tertiaryButtonEl.id; } focusLink() { + let tabList = this.getRootNode().host; this.mainEl.focus(); - return this.mainEl.id; + tabList.currentActiveElementId = this.mainEl.id; + } + + moveFocusRight() { + if (this.currentActiveElementId === "fxview-tab-row-main") { + this.focusSecondaryButton(); + } else if ( + this.tertiaryButtonEl && + this.currentActiveElementId === "fxview-tab-row-secondary-button" + ) { + this.focusTertiaryButton(); + } + } + + moveFocusLeft() { + if (this.currentActiveElementId === "fxview-tab-row-tertiary-button") { + this.focusSecondaryButton(); + } else { + this.focusLink(); + } } dateFluentArgs(timestamp, dateTimeFormat) { @@ -652,16 +537,6 @@ export class FxviewTabRow extends MozLitElement { return icon; } - getContainerClasses() { - let containerClasses = ["fxview-tab-row-container-indicator", "icon"]; - if (this.containerObj) { - let { icon, color } = this.containerObj; - containerClasses.push(`identity-icon-${icon}`); - containerClasses.push(`identity-color-${color}`); - } - return containerClasses; - } - primaryActionHandler(event) { if ( (event.type == "click" && !event.altKey) || @@ -683,9 +558,6 @@ export class FxviewTabRow extends MozLitElement { secondaryActionHandler(event) { if ( - (this.pinnedTabsGridView && - this.indicators?.includes("pinned") && - event.type == "contextmenu") || (event.type == "click" && event.detail && !event.altKey) || // detail=0 is from keyboard (event.type == "click" && !event.detail) @@ -718,92 +590,80 @@ export class FxviewTabRow extends MozLitElement { } } - muteOrUnmuteTab(e) { - e?.preventDefault(); - // If the tab has no sound playing, the mute/unmute button will be removed when toggled. - // We should move the focus to the right in that case. This does not apply to pinned tabs - // on the Open Tabs page. - let shouldMoveFocus = - (!this.pinnedTabsGridView || - (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) && - this.mediaButtonEl && - !this.indicators.includes("soundplaying") && - this.currentActiveElementId === "fxview-tab-row-media-button"; - - // detail=0 is from keyboard - if (e?.type == "click" && !e?.detail && shouldMoveFocus) { - let tabList = this.getRootNode().host; - if (document.dir == "rtl") { - tabList.moveFocusLeft(this); - } else { - tabList.moveFocusRight(this); - } + /** + * Find all matches of query within the given string, and compute the result + * to be rendered. + * + * @param {string} query + * @param {string} string + */ + highlightSearchMatches(query, string) { + const fragments = []; + const regex = RegExp(escapeRegExp(query), "dgi"); + let prevIndexEnd = 0; + let result; + while ((result = regex.exec(string)) !== null) { + const [indexStart, indexEnd] = result.indices[0]; + fragments.push(string.substring(prevIndexEnd, indexStart)); + fragments.push( + html`${string.substring(indexStart, indexEnd)}` + ); + prevIndexEnd = regex.lastIndex; } - this.tabElement.toggleMuteAudio(); + fragments.push(string.substring(prevIndexEnd)); + return fragments; + } + + stylesheets() { + return html``; } - #faviconTemplate() { + faviconTemplate() { return html``; + } + + titleTemplate() { + const title = this.title; + return html` - ${when( - this.pinnedTabsGridView && - this.indicators?.includes("pinned") && - (this.indicators?.includes("muted") || - this.indicators?.includes("soundplaying")), - () => html` - - ` + this.searchQuery, + () => this.highlightSearchMatches(this.searchQuery, title), + () => title )} `; } - #pinnedTabItemTemplate() { - return html` `; + ${when( + this.searchQuery, + () => + this.highlightSearchMatches( + this.searchQuery, + this.formatURIForDisplay(this.url) + ), + () => this.formatURIForDisplay(this.url) + )} + `; } - #unpinnedTabItemTemplate() { - const title = this.title; + dateTemplate() { const relativeString = this.relativeTime( this.time, this.dateTimeFormat, @@ -815,11 +675,81 @@ export class FxviewTabRow extends MozLitElement { !window.IS_STORYBOOK ? this.timeMsPref : NOW_THRESHOLD_MS ); const dateArgs = this.dateFluentArgs(this.time, this.dateTimeFormat); + return html` + + ${relativeString} + `; + } + + timeTemplate() { const timeString = this.timeFluentId(this.dateTimeFormat); const time = this.time; const timeArgs = JSON.stringify({ time }); + return html` + `; + } - return html` html`` + )}`; + } + + tertiaryButtonTemplate() { + return html`${when( + this.tertiaryL10nId && this.tertiaryActionHandler, + () => html`` + )}`; + } +} + +export class FxviewTabRow extends FxviewTabRowBase { + render() { + return html` + ${this.stylesheets()} + - ${this.#faviconTemplate()} - - ${when( - this.searchQuery, - () => this.#highlightSearchMatches(this.searchQuery, title), - () => title - )} - - - - ${when( - this.searchQuery, - () => - this.#highlightSearchMatches( - this.searchQuery, - this.formatURIForDisplay(this.url) - ), - () => this.formatURIForDisplay(this.url) - )} - - - - ${relativeString} - - - + ${this.faviconTemplate()} ${this.titleTemplate()} + ${when( + !this.compact, + () => html`${this.urlTemplate()} ${this.dateTemplate()} + ${this.timeTemplate()}` + )} - ${when( - this.indicators?.includes("soundplaying") || - this.indicators?.includes("muted"), - () => html``, - () => html`` - )} - ${when( - this.secondaryL10nId && this.secondaryActionHandler, - () => html`` - )} - ${when( - this.tertiaryL10nId && this.tertiaryActionHandler, - () => html`` - )}`; - } - - render() { - return html` - ${when( - this.containerObj, - () => html` - - ` - )} - - - ${when( - this.pinnedTabsGridView && this.indicators?.includes("pinned"), - this.#pinnedTabItemTemplate.bind(this), - this.#unpinnedTabItemTemplate.bind(this) - )} + ${this.secondaryButtonTemplate()} ${this.tertiaryButtonTemplate()} `; } - - /** - * Find all matches of query within the given string, and compute the result - * to be rendered. - * - * @param {string} query - * @param {string} string - */ - #highlightSearchMatches(query, string) { - const fragments = []; - const regex = RegExp(escapeRegExp(query), "dgi"); - let prevIndexEnd = 0; - let result; - while ((result = regex.exec(string)) !== null) { - const [indexStart, indexEnd] = result.indices[0]; - fragments.push(string.substring(prevIndexEnd, indexStart)); - fragments.push( - html`${string.substring(indexStart, indexEnd)}` - ); - prevIndexEnd = regex.lastIndex; - } - fragments.push(string.substring(prevIndexEnd)); - return fragments; - } } customElements.define("fxview-tab-row", FxviewTabRow); @@ -1040,10 +810,16 @@ export class VirtualList extends MozLitElement { this.isSubList = false; this.isVisible = false; this.intersectionObserver = new IntersectionObserver( - ([entry]) => (this.isVisible = entry.isIntersecting), + ([entry]) => { + this.isVisible = entry.isIntersecting; + }, { root: this.ownerDocument } ); - this.resizeObserver = new ResizeObserver(([entry]) => { + this.selfResizeObserver = new ResizeObserver(() => { + // Trigger the intersection observer once the tab rows have rendered + this.triggerIntersectionObserver(); + }); + this.childResizeObserver = new ResizeObserver(([entry]) => { if (entry.contentRect?.height > 0) { // Update properties on top-level virtual-list this.parentElement.itemHeightEstimate = entry.contentRect.height; @@ -1058,7 +834,8 @@ export class VirtualList extends MozLitElement { disconnectedCallback() { super.disconnectedCallback(); this.intersectionObserver.disconnect(); - this.resizeObserver.disconnect(); + this.childResizeObserver.disconnect(); + this.selfResizeObserver.disconnect(); } triggerIntersectionObserver() { @@ -1090,7 +867,6 @@ export class VirtualList extends MozLitElement { this.items.slice(i, i + this.maxRenderCountEstimate) ); } - this.triggerIntersectionObserver(); } } @@ -1103,13 +879,17 @@ export class VirtualList extends MozLitElement { firstUpdated() { this.intersectionObserver.observe(this); + this.selfResizeObserver.observe(this); if (this.isSubList && this.children[0]) { - this.resizeObserver.observe(this.children[0]); + this.childResizeObserver.observe(this.children[0]); } } updated(changedProperties) { this.updateListHeight(changedProperties); + if (changedProperties.has("items") && !this.isSubList) { + this.triggerIntersectionObserver(); + } } updateListHeight(changedProperties) { @@ -1157,5 +937,4 @@ export class VirtualList extends MozLitElement { return ""; } } - customElements.define("virtual-list", VirtualList); diff --git a/browser/components/firefoxview/fxview-tab-row.css b/browser/components/firefoxview/fxview-tab-row.css index 219d7e8aa2..c1c8f967a7 100644 --- a/browser/components/firefoxview/fxview-tab-row.css +++ b/browser/components/firefoxview/fxview-tab-row.css @@ -2,9 +2,11 @@ * 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/design-system/text-and-typography.css"); + :host { - --fxviewtabrow-element-background-hover: color-mix(in srgb, currentColor 14%, transparent); - --fxviewtabrow-element-background-active: color-mix(in srgb, currentColor 21%, transparent); + --fxviewtabrow-element-background-hover: var(--button-background-color-ghost-hover); + --fxviewtabrow-element-background-active: var(--button-background-color-ghost-active); display: grid; grid-template-columns: subgrid; grid-column: span 9; @@ -12,7 +14,7 @@ border-radius: 4px; } -@media (prefers-contrast) { +@media (forced-colors) { :host { --fxviewtabrow-element-background-hover: ButtonText; --fxviewtabrow-element-background-active: ButtonText; @@ -32,115 +34,42 @@ cursor: pointer; text-decoration: none; - :host(.pinned) & { - padding: var(--space-small); - min-width: unset; - margin: 0; + :host([compact]) & { + grid-template-columns: min-content auto; } } .fxview-tab-row-main, .fxview-tab-row-main:visited, -.fxview-tab-row-main:hover:active, -.fxview-tab-row-button { +.fxview-tab-row-main:hover:active { color: inherit; } -.fxview-tab-row-main:hover, -.fxview-tab-row-button.ghost-button.icon-button:enabled:hover { +.fxview-tab-row-main:hover { background-color: var(--fxviewtabrow-element-background-hover); color: var(--fxviewtabrow-text-color-hover); - - & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after { - stroke: var(--fxview-indicator-stroke-color-hover); - } } -.fxview-tab-row-main:hover:active, -.fxview-tab-row-button.ghost-button.icon-button:enabled:hover:active { +.fxview-tab-row-main:hover:active { background-color: var(--fxviewtabrow-element-background-active); } -@media (prefers-contrast) { - a.fxview-tab-row-main, - a.fxview-tab-row-main:hover, - a.fxview-tab-row-main:active { +@media (forced-colors) { + .fxview-tab-row-main, + .fxview-tab-row-main:hover, + .fxview-tab-row-main:active { background-color: transparent; border: 1px solid LinkText; color: LinkText; } - a.fxview-tab-row-main:visited, - a.fxview-tab-row-main:visited:hover { + .fxview-tab-row-main:visited, + .fxview-tab-row-main:visited:hover { border: 1px solid VisitedText; color: VisitedText; } } -.fxview-tab-row-favicon-wrapper { - height: 16px; - position: relative; - - .fxview-tab-row-favicon::after, - .fxview-tab-row-button::after, - &.pinned .fxview-tab-row-pinned-media-button { - display: block; - content: ""; - background-size: 12px; - background-position: center; - background-repeat: no-repeat; - position: relative; - height: 12px; - width: 12px; - -moz-context-properties: fill, stroke; - fill: currentColor; - stroke: var(--fxview-background-color-secondary); - } - - &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after { - inset-block-start: 9px; - inset-inline-end: -6px; - } - - &.pinnedOnNewTab .fxview-tab-row-favicon::after, - &.pinnedOnNewTab .fxview-tab-row-button::after { - background-image: url("chrome://browser/skin/pin-12.svg"); - } - - &.bookmark .fxview-tab-row-favicon::after, - &.bookmark .fxview-tab-row-button::after { - background-image: url("chrome://browser/skin/bookmark-12.svg"); - fill: var(--fxview-primary-action-background); - } - - &.attention .fxview-tab-row-favicon::after, - &.attention .fxview-tab-row-button::after { - background-image: radial-gradient(circle, light-dark(rgb(42, 195, 162), rgb(84, 255, 189)), light-dark(rgb(42, 195, 162), rgb(84, 255, 189)) 2px, transparent 2px); - height: 4px; - width: 100%; - inset-block-start: 20px; - } - - &.pinned .fxview-tab-row-pinned-media-button { - inset-block-start: -10px; - inset-inline-end: -10px; - border-radius: 100%; - background-color: var(--fxview-background-color-secondary); - padding: 6px; - min-width: 0; - min-height: 0; - position: absolute; - - &[muted="true"] { - background-image: url("chrome://global/skin/media/audio-muted.svg"); - } - - &[soundplaying="true"] { - background-image: url("chrome://global/skin/media/audio.svg"); - } - } -} - .fxview-tab-row-favicon { background-size: cover; -moz-context-properties: fill; @@ -155,15 +84,6 @@ text-align: match-parent; } -.fxview-tab-row-container-indicator { - height: 16px; - width: 16px; - background-image: var(--identity-icon); - background-size: cover; - -moz-context-properties: fill; - fill: var(--identity-icon-color); -} - .fxview-tab-row-url { color: var(--text-color-deemphasized); text-decoration-line: underline; @@ -182,62 +102,22 @@ font-weight: 400; } -.fxview-tab-row-button { - margin: 0; - cursor: pointer; - min-width: 0; - background-color: transparent; - - &[muted="true"], - &[soundplaying="true"] { - background-size: 16px; - background-repeat: no-repeat; - background-position: center; - -moz-context-properties: fill; - fill: currentColor; - } - - &[muted="true"] { - background-image: url("chrome://global/skin/media/audio-muted.svg"); - } - - &[soundplaying="true"] { - background-image: url("chrome://global/skin/media/audio.svg"); - } - - &.dismiss-button { - background-image: url("chrome://global/skin/icons/close.svg"); - } - - &.options-button { - background-image: url("chrome://global/skin/icons/more.svg"); - } +.fxview-tab-row-button::part(button) { + color: var(--fxview-text-primary-color) } -@media (prefers-contrast) { - .fxview-tab-row-button, - button.fxview-tab-row-main { - border: 1px solid ButtonText; - color: ButtonText; - } +.fxview-tab-row-button[muted="true"]::part(button) { + background-image: url("chrome://global/skin/media/audio-muted.svg"); +} - .fxview-tab-row-button.ghost-button.icon-button:enabled:hover, - button.fxview-tab-row-main:enabled:hover { - border: 1px solid SelectedItem; - color: SelectedItem; - } +.fxview-tab-row-button[soundplaying="true"]::part(button) { + background-image: url("chrome://global/skin/media/audio.svg"); +} - .fxview-tab-row-button.ghost-button.icon-button:enabled:active, - button.fxview-tab-row-main:enabled:active { - color: SelectedItem; - } +.fxview-tab-row-button.dismiss-button::part(button) { + background-image: url("chrome://global/skin/icons/close.svg"); +} - .fxview-tab-row-button.ghost-button.icon-button:enabled, - .fxview-tab-row-button.ghost-button.icon-button:enabled:hover, - .fxview-tab-row-button.ghost-button.icon-button:enabled:active - button.fxview-tab-row-main:enabled, - button.fxview-tab-row-main:enabled:hover, - button.fxview-tab-row-main:enabled:active { - background-color: ButtonFace; - } +.fxview-tab-row-button.options-button::part(button) { + background-image: url("chrome://global/skin/icons/more.svg"); } diff --git a/browser/components/firefoxview/helpers.mjs b/browser/components/firefoxview/helpers.mjs index 3cb308a587..b206deef18 100644 --- a/browser/components/firefoxview/helpers.mjs +++ b/browser/components/firefoxview/helpers.mjs @@ -173,3 +173,20 @@ export function escapeHtmlEntities(text) { .replace(/"/g, """) .replace(/'/g, "'"); } + +export function navigateToLink(e) { + let currentWindow = + e.target.ownerGlobal.browsingContext.embedderWindowGlobal.browsingContext + .window; + if (currentWindow.openTrustedLinkIn) { + let where = lazy.BrowserUtils.whereToOpenLink( + e.detail.originalEvent, + false, + true + ); + if (where == "current") { + where = "tab"; + } + currentWindow.openTrustedLinkIn(e.originalTarget.url, where); + } +} diff --git a/browser/components/firefoxview/history.css b/browser/components/firefoxview/history.css index dd2786a8c7..a10291ddb5 100644 --- a/browser/components/firefoxview/history.css +++ b/browser/components/firefoxview/history.css @@ -51,19 +51,8 @@ cursor: pointer; } -.import-history-banner .close { +moz-button.close::part(button) { background-image: url("chrome://global/skin/icons/close-12.svg"); - background-repeat: no-repeat; - background-position: center center; - -moz-context-properties: fill; - fill: currentColor; - min-width: auto; - min-height: auto; - width: 24px; - height: 24px; - margin: 0; - padding: 0; - flex-shrink: 0; } dialog { diff --git a/browser/components/firefoxview/history.mjs b/browser/components/firefoxview/history.mjs index 1fe028449b..478422d49b 100644 --- a/browser/components/firefoxview/history.mjs +++ b/browser/components/firefoxview/history.mjs @@ -7,18 +7,21 @@ import { ifDefined, when, } from "chrome://global/content/vendor/lit.all.mjs"; -import { escapeHtmlEntities, isSearchEnabled } from "./helpers.mjs"; +import { + escapeHtmlEntities, + isSearchEnabled, + navigateToLink, +} from "./helpers.mjs"; import { ViewPage } from "./viewpage.mjs"; // eslint-disable-next-line import/no-unassigned-import import "chrome://browser/content/migration/migration-wizard.mjs"; +import { HistoryController } from "./HistoryController.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://global/content/elements/moz-button.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { - BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", - FirefoxViewPlacesQuery: - "resource:///modules/firefox-view-places-query.sys.mjs", - PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", }); @@ -26,13 +29,6 @@ let XPCOMUtils = ChromeUtils.importESModule( "resource://gre/modules/XPCOMUtils.sys.mjs" ).XPCOMUtils; -XPCOMUtils.defineLazyPreferenceGetter( - lazy, - "maxRowsPref", - "browser.firefox-view.max-history-rows", - -1 -); - const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart"; const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history"; const IMPORT_HISTORY_DISMISSED_PREF = @@ -44,35 +40,30 @@ class HistoryInView extends ViewPage { constructor() { super(); this._started = false; - this.allHistoryItems = new Map(); - this.historyMapByDate = []; - this.historyMapBySite = []; // Setting maxTabsLength to -1 for no max this.maxTabsLength = -1; - this.placesQuery = new lazy.FirefoxViewPlacesQuery(); - this.searchQuery = ""; - this.searchResults = null; - this.sortOption = "date"; this.profileAge = 8; this.fullyUpdated = false; this.cumulativeSearches = 0; } + controller = new HistoryController(this, { + searchResultsLimit: SEARCH_RESULTS_LIMIT, + }); + start() { if (this._started) { return; } this._started = true; - this.#updateAllHistoryItems(); - this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data)); + this.controller.updateAllHistoryItems(); this.toggleVisibilityInCardContainer(); } async connectedCallback() { super.connectedCallback(); - await this.updateHistoryData(); XPCOMUtils.defineLazyPreferenceGetter( this, "importHistoryDismissedPref", @@ -91,6 +82,7 @@ class HistoryInView extends ViewPage { this.requestUpdate(); } ); + if (!this.importHistoryDismissedPref && !this.hasImportedHistoryPrefs) { let profileAccessor = await lazy.ProfileAge(); let profileCreateTime = await profileAccessor.created; @@ -106,7 +98,6 @@ class HistoryInView extends ViewPage { return; } this._started = false; - this.placesQuery.close(); this.toggleVisibilityInCardContainer(); } @@ -120,32 +111,6 @@ class HistoryInView extends ViewPage { ); } - async #updateAllHistoryItems(allHistoryItems) { - if (allHistoryItems) { - this.allHistoryItems = allHistoryItems; - } else { - await this.updateHistoryData(); - } - this.resetHistoryMaps(); - this.lists.forEach(list => list.requestUpdate()); - await this.#updateSearchResults(); - } - - async #updateSearchResults() { - if (this.searchQuery) { - try { - this.searchResults = await this.placesQuery.searchHistory( - this.searchQuery, - SEARCH_RESULTS_LIMIT - ); - } catch (e) { - // Connection interrupted, ignore. - } - } else { - this.searchResults = null; - } - } - viewVisibleCallback() { this.start(); } @@ -166,14 +131,8 @@ class HistoryInView extends ViewPage { }; static properties = { - ...ViewPage.properties, - allHistoryItems: { type: Map }, - historyMapByDate: { type: Array }, - historyMapBySite: { type: Array }, // Making profileAge a reactive property for testing profileAge: { type: Number }, - searchResults: { type: Array }, - sortOption: { type: String }, }; async getUpdateComplete() { @@ -181,70 +140,8 @@ class HistoryInView extends ViewPage { await Promise.all(Array.from(this.cards).map(card => card.updateComplete)); } - async updateHistoryData() { - this.allHistoryItems = await this.placesQuery.getHistory({ - daysOld: 60, - limit: lazy.maxRowsPref, - sortBy: this.sortOption, - }); - } - - resetHistoryMaps() { - this.historyMapByDate = []; - this.historyMapBySite = []; - } - - createHistoryMaps() { - if (this.sortOption === "date" && !this.historyMapByDate.length) { - const { - visitsFromToday, - visitsFromYesterday, - visitsByDay, - visitsByMonth, - } = this.placesQuery; - - // Add visits from today and yesterday. - if (visitsFromToday.length) { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-today", - items: visitsFromToday, - }); - } - if (visitsFromYesterday.length) { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-yesterday", - items: visitsFromYesterday, - }); - } - - // Add visits from this month, grouped by day. - visitsByDay.forEach(visits => { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-this-month", - items: visits, - }); - }); - - // Add visits from previous months, grouped by month. - visitsByMonth.forEach(visits => { - this.historyMapByDate.push({ - l10nId: "firefoxview-history-date-prev-month", - items: visits, - }); - }); - } else if (this.sortOption === "site" && !this.historyMapBySite.length) { - this.historyMapBySite = Array.from( - this.allHistoryItems.entries(), - ([domain, items]) => ({ - domain, - items, - l10nId: domain ? null : "firefoxview-history-site-localhost", - }) - ).sort((a, b) => a.domain.localeCompare(b.domain)); - } - } - onPrimaryAction(e) { + navigateToLink(e); // Record telemetry Services.telemetry.recordEvent( "firefoxview_next", @@ -254,26 +151,13 @@ class HistoryInView extends ViewPage { {} ); - if (this.searchQuery) { + if (this.controller.searchQuery) { const searchesHistogram = Services.telemetry.getKeyedHistogramById( "FIREFOX_VIEW_CUMULATIVE_SEARCHES" ); searchesHistogram.add("history", this.cumulativeSearches); this.cumulativeSearches = 0; } - - let currentWindow = this.getWindow(); - if (currentWindow.openTrustedLinkIn) { - let where = lazy.BrowserUtils.whereToOpenLink( - e.detail.originalEvent, - false, - true - ); - if (where == "current") { - where = "tab"; - } - currentWindow.openTrustedLinkIn(e.originalTarget.url, where); - } } onSecondaryAction(e) { @@ -282,24 +166,29 @@ class HistoryInView extends ViewPage { } deleteFromHistory(e) { - lazy.PlacesUtils.history.remove(this.triggerNode.url); + this.controller.deleteFromHistory(); this.recordContextMenuTelemetry("delete-from-history", e); } async onChangeSortOption(e) { - this.sortOption = e.target.value; + await this.controller.onChangeSortOption(e); Services.telemetry.recordEvent( "firefoxview_next", "sort_history", "tabs", null, { - sort_type: this.sortOption, - search_start: this.searchQuery ? "true" : "false", + sort_type: this.controller.sortOption, + search_start: this.controller.searchQuery ? "true" : "false", } ); - await this.updateHistoryData(); - await this.#updateSearchResults(); + } + + async onSearchQuery(e) { + await this.controller.onSearchQuery(e); + this.cumulativeSearches = this.controller.searchQuery + ? this.cumulativeSearches + 1 + : 0; } showAllHistory() { @@ -396,9 +285,9 @@ class HistoryInView extends ViewPage { * The template to use for cards-container. */ get cardsTemplate() { - if (this.searchResults) { + if (this.controller.searchResults) { return this.#searchResultsTemplate(); - } else if (this.allHistoryItems.size) { + } else if (this.controller.allHistoryItems.size) { return this.#historyCardsTemplate(); } return this.#emptyMessageTemplate(); @@ -406,8 +295,11 @@ class HistoryInView extends ViewPage { #historyCardsTemplate() { let cardsTemplate = []; - if (this.sortOption === "date" && this.historyMapByDate.length) { - this.historyMapByDate.forEach(historyItem => { + if ( + this.controller.sortOption === "date" && + this.controller.historyMapByDate.length + ) { + this.controller.historyMapByDate.forEach(historyItem => { if (historyItem.items.length) { let dateArg = JSON.stringify({ date: historyItem.items[0].time }); cardsTemplate.push(html` @@ -424,7 +316,7 @@ class HistoryInView extends ViewPage { : "time"} hasPopup="menu" maxTabsLength=${this.maxTabsLength} - .tabItems=${historyItem.items} + .tabItems=${[...historyItem.items]} @fxview-tab-list-primary-action=${this.onPrimaryAction} @fxview-tab-list-secondary-action=${this.onSecondaryAction} > @@ -433,8 +325,8 @@ class HistoryInView extends ViewPage { `); } }); - } else if (this.historyMapBySite.length) { - this.historyMapBySite.forEach(historyItem => { + } else if (this.controller.historyMapBySite.length) { + this.controller.historyMapBySite.forEach(historyItem => { if (historyItem.items.length) { cardsTemplate.push(html`

@@ -446,7 +338,7 @@ class HistoryInView extends ViewPage { dateTimeFormat="dateTime" hasPopup="menu" maxTabsLength=${this.maxTabsLength} - .tabItems=${historyItem.items} + .tabItems=${[...historyItem.items]} @fxview-tab-list-primary-action=${this.onPrimaryAction} @fxview-tab-list-secondary-action=${this.onSecondaryAction} > @@ -504,17 +396,17 @@ class HistoryInView extends ViewPage { slot="header" data-l10n-id="firefoxview-search-results-header" data-l10n-args=${JSON.stringify({ - query: escapeHtmlEntities(this.searchQuery), + query: escapeHtmlEntities(this.controller.searchQuery), })} >

${when( - this.searchResults.length, + this.controller.searchResults.length, () => html`

` )} @@ -524,10 +416,11 @@ class HistoryInView extends ViewPage { dateTimeFormat="dateTime" hasPopup="menu" maxTabsLength="-1" - .searchQuery=${this.searchQuery} - .tabItems=${this.searchResults} + .searchQuery=${this.controller.searchQuery} + .tabItems=${this.controller.searchResults} @fxview-tab-list-primary-action=${this.onPrimaryAction} @fxview-tab-list-secondary-action=${this.onSecondaryAction} + .searchInProgress=${this.controller.placesQuery.searchInProgress} > ${this.panelListTemplate()} @@ -569,7 +462,7 @@ class HistoryInView extends ViewPage { id="sort-by-date" name="history-sort-option" value="date" - ?checked=${this.sortOption === "date"} + ?checked=${this.controller.sortOption === "date"} @click=${this.onChangeSortOption} />
@@ -624,32 +518,24 @@ class HistoryInView extends ViewPage { `; } - async onSearchQuery(e) { - this.searchQuery = e.detail.query; - this.cumulativeSearches = this.searchQuery - ? this.cumulativeSearches + 1 - : 0; - this.#updateSearchResults(); - } - - willUpdate(changedProperties) { + willUpdate() { this.fullyUpdated = false; - if (this.allHistoryItems.size && !changedProperties.has("sortOption")) { + if (this.controller.allHistoryItems.size) { // onChangeSortOption() will update history data once it has been fetched // from the API. - this.createHistoryMaps(); + this.controller.createHistoryMaps(); } } } diff --git a/browser/components/firefoxview/jar.mn b/browser/components/firefoxview/jar.mn index 1e5cc3e690..8bf3597aa5 100644 --- a/browser/components/firefoxview/jar.mn +++ b/browser/components/firefoxview/jar.mn @@ -9,6 +9,7 @@ browser.jar: content/browser/firefoxview/firefoxview.mjs content/browser/firefoxview/history.css content/browser/firefoxview/history.mjs + content/browser/firefoxview/HistoryController.mjs content/browser/firefoxview/opentabs.mjs content/browser/firefoxview/view-opentabs.css content/browser/firefoxview/syncedtabs.mjs @@ -23,6 +24,9 @@ browser.jar: content/browser/firefoxview/fxview-tab-list.css content/browser/firefoxview/fxview-tab-list.mjs content/browser/firefoxview/fxview-tab-row.css + content/browser/firefoxview/opentabs-tab-list.css + content/browser/firefoxview/opentabs-tab-list.mjs + content/browser/firefoxview/opentabs-tab-row.css content/browser/firefoxview/recentlyclosed.mjs content/browser/firefoxview/viewpage.mjs content/browser/firefoxview/history-empty.svg (content/history-empty.svg) diff --git a/browser/components/firefoxview/opentabs-tab-list.css b/browser/components/firefoxview/opentabs-tab-list.css new file mode 100644 index 0000000000..9245a0fada --- /dev/null +++ b/browser/components/firefoxview/opentabs-tab-list.css @@ -0,0 +1,32 @@ +/* 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/. */ + +.fxview-tab-list { + &.pinned { + display: flex; + flex-wrap: wrap; + + > virtual-list { + display: block; + } + + > opentabs-tab-row { + display: block; + margin-block-end: var(--space-xsmall); + } + } + + &.hasContainerTab { + grid-template-columns: min-content 3fr min-content 2fr 1fr 1fr min-content min-content; + } +} + +virtual-list { + grid-column: span 9; + + .top-padding, + .bottom-padding { + grid-column: span 9; + } +} diff --git a/browser/components/firefoxview/opentabs-tab-list.mjs b/browser/components/firefoxview/opentabs-tab-list.mjs new file mode 100644 index 0000000000..4b6d6b3c86 --- /dev/null +++ b/browser/components/firefoxview/opentabs-tab-list.mjs @@ -0,0 +1,593 @@ +/* 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, + styleMap, + 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 clickable tab items + * + * @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view + */ + +export class OpenTabsTabList extends FxviewTabListBase { + constructor() { + super(); + this.pinnedTabsGridView = false; + this.pinnedTabs = []; + this.unpinnedTabs = []; + } + + static properties = { + pinnedTabsGridView: { type: Boolean }, + }; + + static queries = { + ...FxviewTabListBase.queries, + rowEls: { + all: "opentabs-tab-row", + }, + }; + + willUpdate(changes) { + this.activeIndex = Math.min( + Math.max(this.activeIndex, 0), + this.tabItems.length - 1 + ); + + if (changes.has("dateTimeFormat") || changes.has("updatesPaused")) { + this.clearIntervalTimer(); + if (!this.updatesPaused && this.dateTimeFormat == "relative") { + this.startIntervalTimer(); + this.onIntervalUpdate(); + } + } + + // Move pinned tabs to the beginning of the list + if (this.pinnedTabsGridView) { + // Can set maxTabsLength to -1 to have no max + this.unpinnedTabs = this.tabItems.filter( + tab => !tab.indicators.includes("pinned") + ); + this.pinnedTabs = this.tabItems.filter(tab => + tab.indicators.includes("pinned") + ); + if (this.maxTabsLength > 0) { + this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength); + } + this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs]; + } else if (this.maxTabsLength > 0) { + this.tabItems = this.tabItems.slice(0, this.maxTabsLength); + } + } + + /** + * Focuses the expected element (either the link or button) within fxview-tab-row + * The currently focused/active element ID within a row is stored in this.currentActiveElementId + */ + handleFocusElementInRow(e) { + let fxviewTabRow = e.target; + if (e.code == "ArrowUp") { + // Focus either the link or button of the previous row based on this.currentActiveElementId + e.preventDefault(); + if ( + (this.pinnedTabsGridView && + this.activeIndex >= this.pinnedTabs.length) || + !this.pinnedTabsGridView + ) { + this.focusPrevRow(); + } + } else if (e.code == "ArrowDown") { + // Focus either the link or button of the next row based on this.currentActiveElementId + e.preventDefault(); + if ( + this.pinnedTabsGridView && + this.activeIndex < this.pinnedTabs.length + ) { + this.focusIndex(this.pinnedTabs.length); + } else { + this.focusNextRow(); + } + } else if (e.code == "ArrowRight") { + // Focus either the link or the button in the current row and + // set this.currentActiveElementId to that element's ID + e.preventDefault(); + if (document.dir == "rtl") { + fxviewTabRow.moveFocusLeft(); + } else { + fxviewTabRow.moveFocusRight(); + } + } else if (e.code == "ArrowLeft") { + // Focus either the link or the button in the current row and + // set this.currentActiveElementId to that element's ID + e.preventDefault(); + if (document.dir == "rtl") { + fxviewTabRow.moveFocusRight(); + } else { + fxviewTabRow.moveFocusLeft(); + } + } + } + + async focusIndex(index) { + // Focus link or button of item + if ( + ((this.pinnedTabsGridView && index > this.pinnedTabs.length) || + !this.pinnedTabsGridView) && + lazy.virtualListEnabledPref + ) { + let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length); + if (!row) { + return; + } + let subList = this.rootVirtualListEl.getSubListForItem( + index - this.pinnedTabs.length + ); + if (!subList) { + return; + } + this.activeIndex = index; + + // In Bug 1866845, these manual updates to the sublists should be removed + // and scrollIntoView() should also be iterated on so that we aren't constantly + // moving the focused item to the center of the viewport + for (const sublist of Array.from(this.rootVirtualListEl.children)) { + await sublist.requestUpdate(); + await sublist.updateComplete; + } + row.scrollIntoView({ block: "center" }); + row.focus(); + } else if (index >= 0 && index < this.rowEls?.length) { + this.rowEls[index].focus(); + this.activeIndex = index; + } + } + + #getTabListWrapperClasses() { + let wrapperClasses = ["fxview-tab-list"]; + let tabsToCheck = this.pinnedTabsGridView + ? this.unpinnedTabs + : this.tabItems; + if (tabsToCheck.some(tab => tab.containerObj)) { + wrapperClasses.push(`hasContainerTab`); + } + return wrapperClasses; + } + + itemTemplate = (tabItem, i) => { + let time; + if (tabItem.time || tabItem.closedAt) { + let stringTime = (tabItem.time || tabItem.closedAt).toString(); + // Different APIs return time in different units, so we use + // the length to decide if it's milliseconds or nanoseconds. + if (stringTime.length === 16) { + time = (tabItem.time || tabItem.closedAt) / 1000; + } else { + time = tabItem.time || tabItem.closedAt; + } + } + + return html``; + }; + + render() { + if (this.searchQuery && this.tabItems.length === 0) { + return this.emptySearchResultsTemplate(); + } + return html` + ${this.stylesheets()} + + ${when( + this.pinnedTabsGridView && this.pinnedTabs.length, + () => html` +
+ ${this.pinnedTabs.map((tabItem, i) => + this.customItemTemplate + ? this.customItemTemplate(tabItem, i) + : this.itemTemplate(tabItem, i) + )} +
+ ` + )} +
+ ${when( + lazy.virtualListEnabledPref, + () => html` + + `, + () => + html`${this.tabItems.map((tabItem, i) => + this.itemTemplate(tabItem, i) + )}` + )} +
+ + `; + } +} +customElements.define("opentabs-tab-list", OpenTabsTabList); + +/** + * A tab item that displays favicon, title, url, and time of last access + * + * @property {object} containerObj - Info about an open tab's container if within one + * @property {string} indicators - An array of tab indicators if any are present + * @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view + */ + +export class OpenTabsTabRow extends FxviewTabRowBase { + constructor() { + super(); + this.indicators = []; + this.pinnedTabsGridView = false; + } + + static properties = { + ...FxviewTabRowBase.properties, + containerObj: { type: Object }, + indicators: { type: Array }, + pinnedTabsGridView: { type: Boolean }, + }; + + static queries = { + ...FxviewTabRowBase.queries, + mediaButtonEl: "#fxview-tab-row-media-button", + pinnedTabButtonEl: "moz-button#fxview-tab-row-main", + }; + + connectedCallback() { + super.connectedCallback(); + this.addEventListener("keydown", this.handleKeydown); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.removeEventListener("keydown", this.handleKeydown); + } + + handleKeydown(e) { + if ( + this.active && + this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + e.key === "m" && + e.ctrlKey + ) { + this.muteOrUnmuteTab(); + } + } + + moveFocusRight() { + let tabList = this.getRootNode().host; + if (this.pinnedTabsGridView && this.indicators?.includes("pinned")) { + tabList.focusNextRow(); + } else if ( + (this.indicators?.includes("soundplaying") || + this.indicators?.includes("muted")) && + this.currentActiveElementId === "fxview-tab-row-main" + ) { + this.focusMediaButton(); + } else if ( + this.currentActiveElementId === "fxview-tab-row-media-button" || + this.currentActiveElementId === "fxview-tab-row-main" + ) { + this.focusSecondaryButton(); + } else if ( + this.tertiaryButtonEl && + this.currentActiveElementId === "fxview-tab-row-secondary-button" + ) { + this.focusTertiaryButton(); + } + } + + moveFocusLeft() { + let tabList = this.getRootNode().host; + if ( + this.pinnedTabsGridView && + (this.indicators?.includes("pinned") || + (tabList.currentActiveElementId === "fxview-tab-row-main" && + tabList.activeIndex === tabList.pinnedTabs.length)) + ) { + tabList.focusPrevRow(); + } else if ( + tabList.currentActiveElementId === "fxview-tab-row-tertiary-button" + ) { + this.focusSecondaryButton(); + } else if ( + (this.indicators?.includes("soundplaying") || + this.indicators?.includes("muted")) && + tabList.currentActiveElementId === "fxview-tab-row-secondary-button" + ) { + this.focusMediaButton(); + } else { + this.focusLink(); + } + } + + focusMediaButton() { + let tabList = this.getRootNode().host; + this.mediaButtonEl.focus(); + tabList.currentActiveElementId = this.mediaButtonEl.id; + } + + #secondaryActionHandler(event) { + if ( + (this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + event.type == "contextmenu") || + (event.type == "click" && event.detail && !event.altKey) || + // detail=0 is from keyboard + (event.type == "click" && !event.detail) + ) { + event.preventDefault(); + this.dispatchEvent( + new CustomEvent("fxview-tab-list-secondary-action", { + bubbles: true, + composed: true, + detail: { originalEvent: event, item: this }, + }) + ); + } + } + + #faviconTemplate() { + return html` + + ${when( + this.pinnedTabsGridView && + this.indicators?.includes("pinned") && + (this.indicators?.includes("muted") || + this.indicators?.includes("soundplaying")), + () => html` + + ` + )} + `; + } + + #getContainerClasses() { + let containerClasses = ["fxview-tab-row-container-indicator", "icon"]; + if (this.containerObj) { + let { icon, color } = this.containerObj; + containerClasses.push(`identity-icon-${icon}`); + containerClasses.push(`identity-color-${color}`); + } + return containerClasses; + } + + muteOrUnmuteTab(e) { + e?.preventDefault(); + // If the tab has no sound playing, the mute/unmute button will be removed when toggled. + // We should move the focus to the right in that case. This does not apply to pinned tabs + // on the Open Tabs page. + let shouldMoveFocus = + (!this.pinnedTabsGridView || + (!this.indicators.includes("pinned") && this.pinnedTabsGridView)) && + this.mediaButtonEl && + !this.indicators.includes("soundplaying") && + this.currentActiveElementId === "fxview-tab-row-media-button"; + + // detail=0 is from keyboard + if (e?.type == "click" && !e?.detail && shouldMoveFocus) { + if (document.dir == "rtl") { + this.moveFocusLeft(); + } else { + this.moveFocusRight(); + } + } + this.tabElement.toggleMuteAudio(); + } + + #mediaButtonTemplate() { + return html`${when( + this.indicators?.includes("soundplaying") || + this.indicators?.includes("muted"), + () => html``, + () => html`` + )}`; + } + + #containerIndicatorTemplate() { + let tabList = this.getRootNode().host; + let tabsToCheck = tabList.pinnedTabsGridView + ? tabList.unpinnedTabs + : tabList.tabItems; + return html`${when( + tabsToCheck.some(tab => tab.containerObj), + () => html`` + )}`; + } + + #pinnedTabItemTemplate() { + return html` + + ${this.#faviconTemplate()} + + `; + } + + #unpinnedTabItemTemplate() { + return html` + ${this.#faviconTemplate()} ${this.titleTemplate()} + ${when( + !this.compact, + () => html`${this.#containerIndicatorTemplate()} ${this.urlTemplate()} + ${this.dateTemplate()} ${this.timeTemplate()}` + )} + + ${this.#mediaButtonTemplate()} ${this.secondaryButtonTemplate()} + ${this.tertiaryButtonTemplate()}`; + } + + render() { + return html` + ${this.stylesheets()} + + ${when( + this.containerObj, + () => html` + + ` + )} + ${when( + this.pinnedTabsGridView && this.indicators?.includes("pinned"), + this.#pinnedTabItemTemplate.bind(this), + this.#unpinnedTabItemTemplate.bind(this) + )} + `; + } +} +customElements.define("opentabs-tab-row", OpenTabsTabRow); diff --git a/browser/components/firefoxview/opentabs-tab-row.css b/browser/components/firefoxview/opentabs-tab-row.css new file mode 100644 index 0000000000..e5c00884b3 --- /dev/null +++ b/browser/components/firefoxview/opentabs-tab-row.css @@ -0,0 +1,119 @@ +/* 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/. */ + +.fxview-tab-row-favicon-wrapper { + height: 16px; + position: relative; + display: block; + + .fxview-tab-row-favicon::after, + .fxview-tab-row-button::after, + &.pinned .fxview-tab-row-pinned-media-button { + display: block; + content: ""; + background-size: 12px; + background-position: center; + background-repeat: no-repeat; + position: relative; + height: 12px; + width: 12px; + -moz-context-properties: fill, stroke; + fill: currentColor; + stroke: var(--fxview-background-color-secondary); + } + + &:is(.pinnedOnNewTab, .bookmark):not(.attention) .fxview-tab-row-favicon::after { + inset-block-start: 9px; + inset-inline-end: -6px; + } + + &.pinnedOnNewTab .fxview-tab-row-favicon::after, + &.pinnedOnNewTab .fxview-tab-row-button::after { + background-image: url("chrome://browser/skin/pin-12.svg"); + } + + &.bookmark .fxview-tab-row-favicon::after, + &.bookmark .fxview-tab-row-button::after { + background-image: url("chrome://browser/skin/bookmark-12.svg"); + fill: var(--fxview-primary-action-background); + } + + &.attention .fxview-tab-row-favicon::after, + &.attention .fxview-tab-row-button::after { + background-image: radial-gradient(circle, var(--attention-dot-color), var(--attention-dot-color) 2px, transparent 2px); + height: 4px; + width: 100%; + inset-block-start: 20px; + } + + &.pinned .fxview-tab-row-pinned-media-button { + inset-block-start: -5px; + inset-inline-end: 1px; + border: var(--button-border); + border-radius: 100%; + background-color: var(--fxview-background-color-secondary); + padding: 6px; + min-width: 0; + min-height: 0; + position: absolute; + + &[muted="true"] { + background-image: url("chrome://global/skin/media/audio-muted.svg"); + } + + &[soundplaying="true"] { + background-image: url("chrome://global/skin/media/audio.svg"); + } + + &:active, + &:hover:active { + background-color: var(--button-background-color-active); + } + } +} + +.fxview-tab-row-container-indicator { + height: 16px; + width: 16px; + background-image: var(--identity-icon); + background-size: cover; + -moz-context-properties: fill; + fill: var(--identity-icon-color); +} + +.fxview-tab-row-main { + :host(.pinned) & { + padding: var(--space-small); + min-width: unset; + margin: 0; + } +} + +button.fxview-tab-row-main:hover { + & .fxview-tab-row-favicon-wrapper .fxview-tab-row-favicon::after { + stroke: var(--fxview-indicator-stroke-color-hover); + } +} + +@media (prefers-contrast) { + button.fxview-tab-row-main { + border: 1px solid ButtonText; + color: ButtonText; + } + + button.fxview-tab-row-main:enabled:hover { + border: 1px solid SelectedItem; + color: SelectedItem; + } + + button.fxview-tab-row-main:enabled:active { + color: SelectedItem; + } + + button.fxview-tab-row-main:enabled, + button.fxview-tab-row-main:enabled:hover, + button.fxview-tab-row-main:enabled:active { + background-color: ButtonFace; + } +} diff --git a/browser/components/firefoxview/opentabs.mjs b/browser/components/firefoxview/opentabs.mjs index 8d7723e931..fb84553e26 100644 --- a/browser/components/firefoxview/opentabs.mjs +++ b/browser/components/firefoxview/opentabs.mjs @@ -17,6 +17,8 @@ import { MAX_TABS_FOR_RECENT_BROWSING, } from "./helpers.mjs"; import { ViewPage, ViewPageContent } from "./viewpage.mjs"; +// eslint-disable-next-line import/no-unassigned-import +import "chrome://browser/content/firefoxview/opentabs-tab-list.mjs"; const lazy = {}; @@ -36,6 +38,9 @@ ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => { ).getFxAccountsSingleton(); }); +const TOPIC_DEVICESTATE_CHANGED = "firefox-view.devicestate.changed"; +const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated"; + /** * A collection of open tabs grouped by window. * @@ -339,7 +344,7 @@ class OpenTabsInView extends ViewPage { >`; } - handleEvent({ detail, target, type }) { + handleEvent({ detail, type }) { if (this.recentBrowsing && type === "fxview-search-textbox-query") { this.onSearchQuery({ detail }); return; @@ -424,7 +429,7 @@ class OpenTabsInViewCard extends ViewPageContent { static queries = { cardEl: "card-container", tabContextMenu: "view-opentabs-contextmenu", - tabList: "fxview-tab-list", + tabList: "opentabs-tab-list", }; openContextMenu(e) { @@ -565,7 +570,7 @@ class OpenTabsInViewCard extends ViewPageContent { () => html`

${this.title}

` )}
- - +
${when( this.recentBrowsing, @@ -659,7 +664,7 @@ customElements.define("view-opentabs-card", OpenTabsInViewCard); class OpenTabsContextMenu extends MozLitElement { static properties = { devices: { type: Array }, - triggerNode: { type: Object }, + triggerNode: { hasChanged: () => true, type: Object }, }; static queries = { @@ -669,6 +674,7 @@ class OpenTabsContextMenu extends MozLitElement { constructor() { super(); this.triggerNode = null; + this.boundObserve = (...args) => this.observe(...args); this.devices = []; } @@ -680,6 +686,28 @@ class OpenTabsContextMenu extends MozLitElement { return this.ownerDocument.querySelector("view-opentabs"); } + connectedCallback() { + super.connectedCallback(); + this.fetchDevicesPromise = this.fetchDevices(); + Services.obs.addObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); + Services.obs.addObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED); + } + + disconnectedCallback() { + super.disconnectedCallback(); + Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICELIST_UPDATED); + Services.obs.removeObserver(this.boundObserve, TOPIC_DEVICESTATE_CHANGED); + } + + observe(_subject, topic, _data) { + if ( + topic == TOPIC_DEVICELIST_UPDATED || + topic == TOPIC_DEVICESTATE_CHANGED + ) { + this.fetchDevicesPromise = this.fetchDevices(); + } + } + async fetchDevices() { const currentWindow = this.ownerViewPage.getWindow(); if (currentWindow?.gSync) { @@ -699,7 +727,7 @@ class OpenTabsContextMenu extends MozLitElement { return; } this.triggerNode = triggerNode; - await this.fetchDevices(); + await this.fetchDevicesPromise; await this.getUpdateComplete(); this.panelList.toggle(originalEvent); } @@ -1022,7 +1050,7 @@ function getTabListItems(tabs, isRecentBrowsing) { ? JSON.stringify({ tabTitle: tab.label }) : null, tabElement: tab, - time: tab.lastAccessed, + time: tab.lastSeenActive, title: tab.label, url, }; diff --git a/browser/components/firefoxview/recentlyclosed.mjs b/browser/components/firefoxview/recentlyclosed.mjs index 83c323256c..7efd8d09f2 100644 --- a/browser/components/firefoxview/recentlyclosed.mjs +++ b/browser/components/firefoxview/recentlyclosed.mjs @@ -65,7 +65,7 @@ class RecentlyClosedTabsInView extends ViewPage { tabList: "fxview-tab-list", }; - observe(subject, topic, data) { + observe(subject, topic) { if ( topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED || (topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH && @@ -249,13 +249,22 @@ class RecentlyClosedTabsInView extends ViewPage { onDismissTab(e) { const closedId = parseInt(e.originalTarget.closedId, 10); const sourceClosedId = parseInt(e.originalTarget.sourceClosedId, 10); - const sourceWindowId = e.originalTarget.souceWindowId; - if (sourceWindowId || !isNaN(sourceClosedId)) { + const sourceWindowId = e.originalTarget.sourceWindowId; + if (!isNaN(sourceClosedId)) { + // the sourceClosedId is an identifier for a now-closed window the tab + // was closed in. lazy.SessionStore.forgetClosedTabById(closedId, { sourceClosedId, + }); + } else if (sourceWindowId) { + // the sourceWindowId is an identifier for a currently-open window the tab + // was closed in. + lazy.SessionStore.forgetClosedTabById(closedId, { sourceWindowId, }); } else { + // without either identifier, SessionStore will need to walk its window collections + // to find the close tab with matching closedId lazy.SessionStore.forgetClosedTabById(closedId); } @@ -387,7 +396,6 @@ class RecentlyClosedTabsInView extends ViewPage { () => html` + Services.telemetry.recordEvent( + "firefoxview_next", + "fxa_mobile", + "sync", + null, + { + has_devices: TabsSetupFlowManager.secondaryDeviceConnected.toString(), + } + ), + signupCallback: () => + Services.telemetry.recordEvent( + "firefoxview_next", + "fxa_continue", + "sync", + null + ), + }); + constructor() { super(); this._started = false; - this.boundObserve = (...args) => this.observe(...args); - this._currentSetupStateIndex = -1; - this.errorState = null; this._id = Math.floor(Math.random() * 10e6); - this.currentSyncedTabs = []; if (this.recentBrowsing) { this.maxTabsLength = MAX_TABS_FOR_RECENT_BROWSING; } else { // Setting maxTabsLength to -1 for no max this.maxTabsLength = -1; } - this.devices = []; this.fullyUpdated = false; - this.searchQuery = ""; this.showAll = false; this.cumulativeSearches = 0; + this.onSearchQuery = this.onSearchQuery.bind(this); } static properties = { ...ViewPage.properties, - errorState: { type: Number }, - currentSyncedTabs: { type: Array }, - _currentSetupStateIndex: { type: Number }, - devices: { type: Array }, - searchQuery: { type: String }, showAll: { type: Boolean }, cumulativeSearches: { type: Number }, }; @@ -72,26 +77,19 @@ class SyncedTabsInView extends ViewPage { tabLists: { all: "fxview-tab-list" }, }; - connectedCallback() { - super.connectedCallback(); - this.addEventListener("click", this); - } - start() { if (this._started) { return; } this._started = true; - Services.obs.addObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED); - Services.obs.addObserver(this.boundObserve, SYNCED_TABS_CHANGED); - - this.updateStates(); + this.controller.addSyncObservers(); + this.controller.updateStates(); this.onVisibilityChange(); if (this.recentBrowsing) { this.recentBrowsingElement.addEventListener( "fxview-search-textbox-query", - this + this.onSearchQuery ); } } @@ -103,75 +101,21 @@ class SyncedTabsInView extends ViewPage { this._started = false; TabsSetupFlowManager.updateViewVisibility(this._id, "unloaded"); this.onVisibilityChange(); - - Services.obs.removeObserver(this.boundObserve, TOPIC_SETUPSTATE_CHANGED); - Services.obs.removeObserver(this.boundObserve, SYNCED_TABS_CHANGED); + this.controller.removeSyncObservers(); if (this.recentBrowsing) { this.recentBrowsingElement.removeEventListener( "fxview-search-textbox-query", - this + this.onSearchQuery ); } } - willUpdate(changedProperties) { - if (changedProperties.has("searchQuery")) { - this.cumulativeSearches = this.searchQuery - ? this.cumulativeSearches + 1 - : 0; - } - } - disconnectedCallback() { super.disconnectedCallback(); this.stop(); } - 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); - } - if (this.recentBrowsing && event.type === "fxview-search-textbox-query") { - this.onSearchQuery(event); - } - } - viewVisibleCallback() { this.start(); } @@ -196,90 +140,16 @@ class SyncedTabsInView extends ViewPage { this.toggleVisibilityInCardContainer(); } - async observe(subject, topic, errorState) { - if (topic == TOPIC_SETUPSTATE_CHANGED) { - this.updateStates(errorState); - } - if (topic == SYNCED_TABS_CHANGED) { - this.getSyncedTabData(); - } - } - - updateStates(errorState) { - let stateIndex = TabsSetupFlowManager.uiStateIndex; - errorState = 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", - descriptionLink: { - name: "url", - url: "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", - buttonLabel: "firefoxview-tabpickup-synctabs-primarybutton", - }, - loading: { - header: "firefoxview-syncedtabs-loading-header", - description: "firefoxview-syncedtabs-loading-description", - }, - }; - - generateMessageCard({ error = false, action, errorState }) { - errorState = errorState || this.errorState; - let header, - description, - descriptionLink, - buttonLabel, - 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; - descriptionLink = this.actionMappings[action].descriptionLink; - mainImageUrl = - "chrome://browser/content/firefoxview/synced-tabs-error.svg"; - descriptionArray = [description]; - } - + generateMessageCard({ + action, + buttonLabel, + descriptionArray, + descriptionLink, + error, + header, + headerIconUrl, + mainImageUrl, + }) { return html` this.controller.handleEvent(e)} aria-details="empty-container" > @@ -307,28 +177,19 @@ class SyncedTabsInView extends ViewPage { } onOpenLink(event) { - let currentWindow = this.getWindow(); - if (currentWindow.openTrustedLinkIn) { - let where = lazy.BrowserUtils.whereToOpenLink( - event.detail.originalEvent, - false, - true - ); - if (where == "current") { - where = "tab"; + navigateToLink(event); + + Services.telemetry.recordEvent( + "firefoxview_next", + "synced_tabs", + "tabs", + null, + { + page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs", } - currentWindow.openTrustedLinkIn(event.originalTarget.url, where); - Services.telemetry.recordEvent( - "firefoxview_next", - "synced_tabs", - "tabs", - null, - { - page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs", - } - ); - } - if (this.searchQuery) { + ); + + if (this.controller.searchQuery) { const searchesHistogram = Services.telemetry.getKeyedHistogramById( "FIREFOX_VIEW_CUMULATIVE_SEARCHES" ); @@ -384,7 +245,7 @@ class SyncedTabsInView extends ViewPage { class="blackbox notabs search-results-empty" data-l10n-id="firefoxview-search-results-empty" data-l10n-args=${JSON.stringify({ - query: escapeHtmlEntities(this.searchQuery), + query: escapeHtmlEntities(this.controller.searchQuery), })} > `, @@ -405,7 +266,8 @@ class SyncedTabsInView extends ViewPage { } onSearchQuery(e) { - this.searchQuery = e.detail.query; + this.controller.searchQuery = e.detail.query; + this.cumulativeSearches = e.detail.query ? this.cumulativeSearches + 1 : 0; this.showAll = false; } @@ -422,7 +284,7 @@ class SyncedTabsInView extends ViewPage { secondaryActionClass="options-button" hasPopup="menu" .tabItems=${ifDefined(tabItems)} - .searchQuery=${this.searchQuery} + .searchQuery=${this.controller.searchQuery} maxTabsLength=${this.showAll ? -1 : this.maxTabsLength} @fxview-tab-list-primary-action=${this.onOpenLink} @fxview-tab-list-secondary-action=${this.onContextMenu} @@ -434,33 +296,9 @@ class SyncedTabsInView extends ViewPage { generateTabList() { let renderArray = []; - let renderInfo = {}; - for (let tab of this.currentSyncedTabs) { - if (!(tab.client in renderInfo)) { - renderInfo[tab.client] = { - name: tab.device, - deviceType: tab.deviceType, - tabs: [], - }; - } - renderInfo[tab.client].tabs.push(tab); - } - - // Add devices without tabs - for (let device of this.devices) { - if (!(device.id in renderInfo)) { - renderInfo[device.id] = { - name: device.name, - deviceType: device.clientType, - tabs: [], - }; - } - } - + let renderInfo = this.controller.getRenderInfo(); for (let id in renderInfo) { - let tabItems = this.searchQuery - ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs)) - : this.getTabItems(renderInfo[id].tabs); + let tabItems = renderInfo[id].tabItems; if (tabItems.length) { const template = this.recentBrowsing ? this.deviceTemplate( @@ -509,7 +347,7 @@ class SyncedTabsInView extends ViewPage { isShowAllLinkVisible(tabItems) { return ( this.recentBrowsing && - this.searchQuery && + this.controller.searchQuery && tabItems.length > this.maxTabsLength && !this.showAll ); @@ -536,35 +374,10 @@ class SyncedTabsInView extends ViewPage { } generateCardContent() { - switch (this._currentSetupStateIndex) { - case 0 /* error-state */: - if (this.errorState) { - return this.generateMessageCard({ error: true }); - } - return this.generateMessageCard({ action: "loading" }); - 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 - return this.generateMessageCard({ - error: true, - errorState: "signed-out", - }); - } - return this.generateMessageCard({ action: "sign-in" }); - case 2 /* connect-secondary-device*/: - return this.generateMessageCard({ action: "add-device" }); - case 3 /* disabled-tab-sync */: - return this.generateMessageCard({ action: "sync-tabs-disabled" }); - case 4 /* synced-tabs-loaded*/: - // There seems to be an edge case where sync says everything worked - // fine but we have no devices. - if (!this.devices.length) { - return this.generateMessageCard({ action: "add-device" }); - } - return this.generateTabList(); - } - return html``; + const cardProperties = this.controller.getMessageCard(); + return cardProperties + ? this.generateMessageCard(cardProperties) + : this.generateTabList(); } render() { @@ -589,7 +402,7 @@ class SyncedTabsInView extends ViewPage { data-l10n-id="firefoxview-synced-tabs-header" > ${when( - isSearchEnabled() || this._currentSetupStateIndex === 4, + isSearchEnabled() || this.controller.currentSetupStateIndex === 4, () => html`
${when( isSearchEnabled(), @@ -606,12 +419,12 @@ class SyncedTabsInView extends ViewPage {
` )} ${when( - this._currentSetupStateIndex === 4, + this.controller.currentSetupStateIndex === 4, () => html`