fune/browser/components/firefoxview/recently-closed-tabs.mjs
Sam Foster a3004a21be Bug 1819675 - rename SessionStore.getClosedTabCount and getClosedTabData to getClosedTabCountForWindow and getClosedTabDataForWindow. r=dao,fxview-reviewers,kcochrane
- As closed tabs will change to mean closed tabs from all windows, rename these functions to make
  changes in later patches clearer when we mean closed tabs from this window specifically, or closed
  tabs for all private/non-private windows

Differential Revision: https://phabricator.services.mozilla.com/D177849
2023-05-30 18:54:12 +00:00

463 lines
13 KiB
JavaScript

/* 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`
<ol class="closed-tabs-list">
${recentlyClosedTabs.map((tab, i) =>
this.recentlyClosedTabTemplate(tab, !i)
)}
</ol>
`;
}
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`
<div
id="recently-closed-tabs-placeholder"
class="placeholder-content"
role="presentation"
>
<img
id="recently-closed-empty-image"
src="chrome://browser/content/firefoxview/recently-closed-empty.svg"
role="presentation"
alt=""
/>
<div class="placeholder-text">
<h4
data-l10n-id="firefoxview-closed-tabs-placeholder-header"
class="placeholder-header"
></h4>
<p
data-l10n-id="firefoxview-closed-tabs-placeholder-body"
class="placeholder-body"
></p>
</div>
</div>
`;
}
recentlyClosedTabTemplate(tab, primary) {
const targetURI = this.getTabStateValue(tab, "url");
const convertedTime = convertTimestamp(
tab.closedAt,
this.fluentStrings,
lazy.timeMsPref
);
return html`
<li
class="closed-tab-li"
data-tabid=${tab.closedId}
data-targeturi=${targetURI}
tabindex=${ifDefined(primary ? null : "-1")}
@contextmenu=${e => (this.contextTriggerNode = e.currentTarget)}
>
<span
class="closed-tab-li-main"
role="button"
tabindex="0"
@click=${e => this.openTabAndUpdate(e)}
@keydown=${e => this.openTabAndUpdate(e)}
>
<div
class="favicon"
style=${styleMap({
backgroundImage: `url(${getImageUrl(tab.icon, targetURI)})`,
})}
></div>
<a
href=${targetURI}
class="closed-tab-li-title"
tabindex="-1"
@click=${e => e.preventDefault()}
>
${tab.title}
</a>
<a
href=${targetURI}
class="closed-tab-li-url"
data-l10n-id="firefoxview-tabs-list-tab-button"
data-l10n-args=${JSON.stringify({ targetURI })}
tabindex="-1"
@click=${e => e.preventDefault()}
>
${formatURIForDisplay(targetURI)}
</a>
<span class="closed-tab-li-time" data-timestamp=${tab.closedAt}>
${convertedTime}
</span>
</span>
<button
class="closed-tab-li-dismiss"
data-l10n-id="firefoxview-closed-tabs-dismiss-tab"
data-l10n-args=${JSON.stringify({ tabTitle: tab.title })}
@click=${e => this.dismissTabAndUpdate(e)}
></button>
</li>
`;
}
// 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",
}
);