From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- browser/components/firefoxview/syncedtabs.mjs | 725 ++++++++++++++++++++++++++ 1 file changed, 725 insertions(+) create mode 100644 browser/components/firefoxview/syncedtabs.mjs (limited to 'browser/components/firefoxview/syncedtabs.mjs') diff --git a/browser/components/firefoxview/syncedtabs.mjs b/browser/components/firefoxview/syncedtabs.mjs new file mode 100644 index 0000000000..5320f8cb41 --- /dev/null +++ b/browser/components/firefoxview/syncedtabs.mjs @@ -0,0 +1,725 @@ +/* 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, { + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", + SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", +}); + +const { SyncedTabsErrorHandler } = ChromeUtils.importESModule( + "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs" +); +const { TabsSetupFlowManager } = ChromeUtils.importESModule( + "resource:///modules/firefox-view-tabs-setup-manager.sys.mjs" +); + +import { + html, + ifDefined, + when, +} from "chrome://global/content/vendor/lit.all.mjs"; +import { ViewPage } from "./viewpage.mjs"; +import { + escapeHtmlEntities, + isSearchEnabled, + searchTabList, + MAX_TABS_FOR_RECENT_BROWSING, +} from "./helpers.mjs"; + +const SYNCED_TABS_CHANGED = "services.sync.tabs.changed"; +const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed"; +const UI_OPEN_STATE = "browser.tabs.firefox-view.ui-state.tab-pickup.open"; + +class SyncedTabsInView extends ViewPage { + 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; + } + + 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 }, + }; + + static queries = { + cardEls: { all: "card-container" }, + emptyState: "fxview-empty-state", + searchTextbox: "fxview-search-textbox", + 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.onVisibilityChange(); + + if (this.recentBrowsing) { + this.recentBrowsingElement.addEventListener( + "fxview-search-textbox-query", + this + ); + } + } + + stop() { + if (!this._started) { + return; + } + 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); + + if (this.recentBrowsing) { + this.recentBrowsingElement.removeEventListener( + "fxview-search-textbox-query", + this + ); + } + } + + 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(); + } + + viewHiddenCallback() { + this.stop(); + } + + onVisibilityChange() { + const isOpen = this.open; + const isVisible = this.isVisible; + if (isVisible && isOpen) { + this.update(); + TabsSetupFlowManager.updateViewVisibility(this._id, "visible"); + } else { + TabsSetupFlowManager.updateViewVisibility( + this._id, + isVisible ? "closed" : "hidden" + ); + } + + 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]; + } + + return html` + + + + `; + } + + onOpenLink(event) { + let currentWindow = this.getWindow(); + if (currentWindow.openTrustedLinkIn) { + let where = lazy.BrowserUtils.whereToOpenLink( + event.detail.originalEvent, + false, + true + ); + if (where == "current") { + where = "tab"; + } + currentWindow.openTrustedLinkIn(event.originalTarget.url, where); + Services.telemetry.recordEvent( + "firefoxview_next", + "synced_tabs", + "tabs", + null, + { + page: this.recentBrowsing ? "recentbrowsing" : "syncedtabs", + } + ); + } + if (this.searchQuery) { + const searchesHistogram = Services.telemetry.getKeyedHistogramById( + "FIREFOX_VIEW_CUMULATIVE_SEARCHES" + ); + searchesHistogram.add( + this.recentBrowsing ? "recentbrowsing" : "syncedtabs", + this.cumulativeSearches + ); + this.cumulativeSearches = 0; + } + } + + onContextMenu(e) { + this.triggerNode = e.originalTarget; + e.target.querySelector("panel-list").toggle(e.detail.originalEvent); + } + + panelListTemplate() { + return html` + + + +
+ +
+ `; + } + + noDeviceTabsTemplate(deviceName, deviceType, isSearchResultsEmpty = false) { + const template = html`

+ + ${deviceName} +

+ ${when( + isSearchResultsEmpty, + () => html` +
+ `, + () => html` +
+ ` + )}`; + return this.recentBrowsing + ? template + : html`${template}`; + } + + onSearchQuery(e) { + this.searchQuery = e.detail.query; + this.showAll = false; + } + + deviceTemplate(deviceName, deviceType, tabItems) { + return html`

+ + ${deviceName} +

+ + ${this.panelListTemplate()} + `; + } + + 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: [], + }; + } + } + + for (let id in renderInfo) { + let tabItems = this.searchQuery + ? searchTabList(this.searchQuery, this.getTabItems(renderInfo[id].tabs)) + : this.getTabItems(renderInfo[id].tabs); + if (tabItems.length) { + const template = this.recentBrowsing + ? this.deviceTemplate( + renderInfo[id].name, + renderInfo[id].deviceType, + tabItems + ) + : html`${this.deviceTemplate( + renderInfo[id].name, + renderInfo[id].deviceType, + tabItems + )} + `; + renderArray.push(template); + if (this.isShowAllLinkVisible(tabItems)) { + renderArray.push(html` `); + } + } else { + // Check renderInfo[id].tabs.length to determine whether to display an + // empty tab list message or empty search results message. + // If there are no synced tabs, we always display the empty tab list + // message, even if there is an active search query. + renderArray.push( + this.noDeviceTabsTemplate( + renderInfo[id].name, + renderInfo[id].deviceType, + Boolean(renderInfo[id].tabs.length) + ) + ); + } + } + return renderArray; + } + + isShowAllLinkVisible(tabItems) { + return ( + this.recentBrowsing && + this.searchQuery && + tabItems.length > this.maxTabsLength && + !this.showAll + ); + } + + enableShowAll(event) { + if ( + event.type == "click" || + (event.type == "keydown" && event.code == "Enter") || + (event.type == "keydown" && event.code == "Space") + ) { + event.preventDefault(); + this.showAll = true; + Services.telemetry.recordEvent( + "firefoxview_next", + "search_show_all", + "showallbutton", + null, + { + section: "syncedtabs", + } + ); + } + } + + 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``; + } + + render() { + this.open = + !TabsSetupFlowManager.isTabSyncSetupComplete || + Services.prefs.getBoolPref(UI_OPEN_STATE, true); + + let renderArray = []; + renderArray.push(html` `); + renderArray.push(html` `); + + if (!this.recentBrowsing) { + renderArray.push(html`
+ + ${when( + isSearchEnabled() || this._currentSetupStateIndex === 4, + () => html`
+ ${when( + isSearchEnabled(), + () => html`
+ +
` + )} + ${when( + this._currentSetupStateIndex === 4, + () => html` + + ` + )} +
` + )} +
`); + } + + if (this.recentBrowsing) { + renderArray.push( + html` + > +

+
${this.generateCardContent()}
+
` + ); + } else { + renderArray.push( + html`
${this.generateCardContent()}
` + ); + } + return renderArray; + } + + async onReload() { + await TabsSetupFlowManager.syncOnPageReload(); + } + + getTabItems(tabs) { + tabs = tabs || this.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: "fxviewtabrow-options-menu-button", + secondaryL10nArgs: JSON.stringify({ tabTitle: tab.title }), + })); + } + + updateTabsList(syncedTabs) { + if (!syncedTabs.length) { + this.currentSyncedTabs = syncedTabs; + this.sendTabTelemetry(0); + } + + const tabsToRender = syncedTabs; + + // Return early if new tabs are the same as previous ones + if ( + JSON.stringify(tabsToRender) == JSON.stringify(this.currentSyncedTabs) + ) { + return; + } + + this.currentSyncedTabs = tabsToRender; + // Record the full tab count + this.sendTabTelemetry(syncedTabs.length); + } + + async getSyncedTabData() { + this.devices = await lazy.SyncedTabs.getTabClients(); + let tabs = await lazy.SyncedTabs.createRecentTabsList(this.devices, 50, { + removeAllDupes: false, + removeDeviceDupes: true, + }); + + this.updateTabsList(tabs); + } + + updated() { + this.fullyUpdated = true; + this.toggleVisibilityInCardContainer(); + } + + sendTabTelemetry(numTabs) { + /* + Services.telemetry.recordEvent( + "firefoxview_next", + "synced_tabs", + "tabs", + null, + { + count: numTabs.toString(), + } + ); +*/ + } +} +customElements.define("view-syncedtabs", SyncedTabsInView); -- cgit v1.2.3