From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../firefoxview/recently-closed-tabs.mjs | 463 +++++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 browser/components/firefoxview/recently-closed-tabs.mjs (limited to 'browser/components/firefoxview/recently-closed-tabs.mjs') diff --git a/browser/components/firefoxview/recently-closed-tabs.mjs b/browser/components/firefoxview/recently-closed-tabs.mjs new file mode 100644 index 0000000000..295fc03d27 --- /dev/null +++ b/browser/components/firefoxview/recently-closed-tabs.mjs @@ -0,0 +1,463 @@ +/* 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, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", +}); + +import { + formatURIForDisplay, + convertTimestamp, + getImageUrl, + onToggleContainer, + NOW_THRESHOLD_MS, +} from "./helpers.mjs"; + +import { + html, + ifDefined, + styleMap, +} from "chrome://global/content/vendor/lit.all.mjs"; +import { MozLitElement } from "chrome://global/content/lit-utils.mjs"; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const SS_NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed"; +const SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH = "sessionstore-browser-shutdown-flush"; +const UI_OPEN_STATE = + "browser.tabs.firefox-view.ui-state.recently-closed-tabs.open"; + +function getWindow() { + return window.browsingContext.embedderWindowGlobal.browsingContext.window; +} + +class RecentlyClosedTabsList extends MozLitElement { + constructor() { + super(); + this.maxTabsLength = 25; + this.recentlyClosedTabs = []; + this.lastFocusedIndex = -1; + + // The recency timestamp update period is stored in a pref to allow tests to easily change it + XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "timeMsPref", + "browser.tabs.firefox-view.updateTimeMs", + NOW_THRESHOLD_MS, + timeMsPref => { + clearInterval(this.intervalID); + this.intervalID = setInterval(() => this.requestUpdate(), timeMsPref); + this.requestUpdate(); + } + ); + } + + createRenderRoot() { + return this; + } + + static queries = { + tabsList: "ol", + timeElements: { all: "span.closed-tab-li-time" }, + }; + + get fluentStrings() { + if (!this._fluentStrings) { + this._fluentStrings = new Localization(["browser/firefoxView.ftl"], true); + } + return this._fluentStrings; + } + + connectedCallback() { + super.connectedCallback(); + this.intervalID = setInterval(() => this.requestUpdate(), lazy.timeMsPref); + } + + disconnectedCallback() { + clearInterval(this.intervalID); + } + + getTabStateValue(tab, key) { + let value = ""; + const tabEntries = tab.state.entries; + const activeIndex = tab.state.index - 1; + + if (activeIndex >= 0 && tabEntries[activeIndex]) { + value = tabEntries[activeIndex][key]; + } + + return value; + } + + openTabAndUpdate(event) { + if ( + (event.type == "click" && !event.altKey) || + (event.type == "keydown" && event.code == "Enter") || + (event.type == "keydown" && event.code == "Space") + ) { + const item = event.target.closest(".closed-tab-li"); + // only used for telemetry + const position = [...this.tabsList.children].indexOf(item) + 1; + const closedId = item.dataset.tabid; + + lazy.SessionStore.undoCloseById(closedId); + + // record telemetry + let tabClosedAt = parseInt( + item.querySelector(".closed-tab-li-time").getAttribute("data-timestamp") + ); + + let now = Date.now(); + let deltaSeconds = (now - tabClosedAt) / 1000; + Services.telemetry.recordEvent( + "firefoxview", + "recently_closed", + "tabs", + null, + { + position: position.toString(), + delta: deltaSeconds.toString(), + } + ); + } + } + + dismissTabAndUpdate(event) { + event.preventDefault(); + const item = event.target.closest(".closed-tab-li"); + this.dismissTabAndUpdateForElement(item); + } + + dismissTabAndUpdateForElement(item) { + let recentlyClosedList = lazy.SessionStore.getClosedTabDataForWindow( + getWindow() + ); + let closedTabIndex = recentlyClosedList.findIndex(closedTab => { + return closedTab.closedId === parseInt(item.dataset.tabid, 10); + }); + if (closedTabIndex < 0) { + // Tab not found in recently closed list + return; + } + lazy.SessionStore.forgetClosedTab(getWindow(), closedTabIndex); + + // record telemetry + let tabClosedAt = parseInt( + item.querySelector(".closed-tab-li-time").dataset.timestamp + ); + + let now = Date.now(); + let deltaSeconds = (now - tabClosedAt) / 1000; + Services.telemetry.recordEvent( + "firefoxview", + "dismiss_closed_tab", + "tabs", + null, + { + delta: deltaSeconds.toString(), + } + ); + } + + updateRecentlyClosedTabs() { + let recentlyClosedTabsData = lazy.SessionStore.getClosedTabDataForWindow( + getWindow() + ); + this.recentlyClosedTabs = recentlyClosedTabsData.slice( + 0, + this.maxTabsLength + ); + this.requestUpdate(); + } + + render() { + let { recentlyClosedTabs } = this; + let closedTabsContainer = document.getElementById( + "recently-closed-tabs-container" + ); + + if (!recentlyClosedTabs.length) { + // Show empty message if no recently closed tabs + closedTabsContainer.toggleContainerStyleForEmptyMsg(true); + return html` ${this.emptyMessageTemplate()} `; + } + + closedTabsContainer.toggleContainerStyleForEmptyMsg(false); + + return html` +
    + ${recentlyClosedTabs.map((tab, i) => + this.recentlyClosedTabTemplate(tab, !i) + )} +
+ `; + } + + willUpdate() { + if (this.tabsList && this.tabsList.contains(document.activeElement)) { + let activeLi = document.activeElement.closest(".closed-tab-li"); + this.lastFocusedIndex = [...this.tabsList.children].indexOf(activeLi); + } else { + this.lastFocusedIndex = -1; + } + } + + updated() { + let focusRestored = false; + if ( + this.lastFocusedIndex >= 0 && + (!this.tabsList || this.lastFocusedIndex >= this.tabsList.children.length) + ) { + if (this.tabsList) { + let items = [...this.tabsList.children]; + let newFocusIndex = items.length - 1; + let newFocus = items[newFocusIndex]; + if (newFocus) { + focusRestored = true; + newFocus.querySelector(".closed-tab-li-main").focus(); + } + } + if (!focusRestored) { + document.getElementById("recently-closed-tabs-header-section").focus(); + } + } + this.lastFocusedIndex = -1; + } + + emptyMessageTemplate() { + return html` + + `; + } + + recentlyClosedTabTemplate(tab, primary) { + const targetURI = this.getTabStateValue(tab, "url"); + const convertedTime = convertTimestamp( + tab.closedAt, + this.fluentStrings, + lazy.timeMsPref + ); + return html` +
  • (this.contextTriggerNode = e.currentTarget)} + > + this.openTabAndUpdate(e)} + @keydown=${e => this.openTabAndUpdate(e)} + > +
    + e.preventDefault()} + > + ${tab.title} + + e.preventDefault()} + > + ${formatURIForDisplay(targetURI)} + + + ${convertedTime} + +
    + +
  • + `; + } + + // Update the URL for a new or previously-populated list item. + // This is needed because when tabs get closed we don't necessarily + // have all the requisite information for them immediately. + updateURLForListItem(li, targetURI) { + li.dataset.targetURI = targetURI; + let urlElement = li.querySelector(".closed-tab-li-url"); + document.l10n.setAttributes( + urlElement, + "firefoxview-tabs-list-tab-button", + { + targetURI, + } + ); + if (targetURI) { + urlElement.textContent = formatURIForDisplay(targetURI); + urlElement.title = targetURI; + } else { + urlElement.textContent = urlElement.title = ""; + } + } +} +customElements.define("recently-closed-tabs-list", RecentlyClosedTabsList); + +class RecentlyClosedTabsContainer extends HTMLDetailsElement { + constructor() { + super(); + this.observerAdded = false; + this.boundObserve = (...args) => this.observe(...args); + } + + connectedCallback() { + this.noTabsElement = this.querySelector( + "#recently-closed-tabs-placeholder" + ); + this.list = this.querySelector("recently-closed-tabs-list"); + this.collapsibleContainer = this.querySelector( + "#collapsible-tabs-container" + ); + this.addEventListener("toggle", this); + getWindow().gBrowser.tabContainer.addEventListener("TabSelect", this); + getWindow().addEventListener("command", this, true); + getWindow() + .document.getElementById("contentAreaContextMenu") + .addEventListener("popuphiding", this); + this.open = Services.prefs.getBoolPref(UI_OPEN_STATE, true); + } + + cleanup() { + getWindow().gBrowser.tabContainer.removeEventListener("TabSelect", this); + getWindow().removeEventListener("command", this, true); + getWindow() + .document.getElementById("contentAreaContextMenu") + .removeEventListener("popuphiding", this); + this.removeObserversIfNeeded(); + } + + addObserversIfNeeded() { + if (!this.observerAdded) { + Services.obs.addObserver( + this.boundObserve, + SS_NOTIFY_CLOSED_OBJECTS_CHANGED + ); + Services.obs.addObserver( + this.boundObserve, + SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH + ); + this.observerAdded = true; + } + } + + removeObserversIfNeeded() { + if (this.observerAdded) { + Services.obs.removeObserver( + this.boundObserve, + SS_NOTIFY_CLOSED_OBJECTS_CHANGED + ); + Services.obs.removeObserver( + this.boundObserve, + SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH + ); + this.observerAdded = false; + } + } + + // we observe when a tab closes but since this notification fires more frequently and on + // all windows, we remove the observer when another tab is selected; we check for changes + // to the session store once the user return to this tab. + handleObservers(contentDocument) { + if (contentDocument?.URL == "about:firefoxview") { + this.addObserversIfNeeded(); + this.list.updateRecentlyClosedTabs(); + } else { + this.removeObserversIfNeeded(); + } + } + + observe(subject, topic, data) { + if ( + topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED || + (topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH && + subject.ownerGlobal == getWindow()) + ) { + this.list.updateRecentlyClosedTabs(); + } + } + + onLoad() { + this.list.updateRecentlyClosedTabs(); + this.addObserversIfNeeded(); + } + + handleEvent(event) { + if (event.type == "toggle") { + onToggleContainer(this); + } else if (event.type == "TabSelect") { + this.handleObservers(event.target.linkedBrowser.contentDocument); + } else if ( + event.type === "command" && + event.target.closest(".context-menu-open-link") && + this.list.contextTriggerNode + ) { + this.list.dismissTabAndUpdateForElement(this.list.contextTriggerNode); + } else if (event.type === "popuphiding") { + delete this.list.contextTriggerNode; + } + } + + toggleContainerStyleForEmptyMsg(visible) { + this.collapsibleContainer.classList.toggle("empty-container", visible); + } + + getClosedTabCount = () => { + try { + return lazy.SessionStore.getClosedTabCountForWindow(getWindow()); + } catch (ex) { + return 0; + } + }; +} +customElements.define( + "recently-closed-tabs-container", + RecentlyClosedTabsContainer, + { + extends: "details", + } +); -- cgit v1.2.3