1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
10 } from "chrome://global/content/vendor/lit.all.mjs";
11 import { MAX_TABS_FOR_RECENT_BROWSING } from "./helpers.mjs";
12 import { searchTabList } from "./search-helpers.mjs";
13 import { ViewPage } from "./viewpage.mjs";
14 // eslint-disable-next-line import/no-unassigned-import
15 import "chrome://browser/content/firefoxview/card-container.mjs";
16 // eslint-disable-next-line import/no-unassigned-import
17 import "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
20 ChromeUtils.defineESModuleGetters(lazy, {
21 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs",
24 const SS_NOTIFY_CLOSED_OBJECTS_CHANGED = "sessionstore-closed-objects-changed";
25 const SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH = "sessionstore-browser-shutdown-flush";
26 const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
27 const INCLUDE_CLOSED_TABS_FROM_CLOSED_WINDOWS =
28 "browser.sessionstore.closedTabsFromClosedWindows";
30 function getWindow() {
31 return window.browsingContext.embedderWindowGlobal.browsingContext.window;
34 class RecentlyClosedTabsInView extends ViewPage {
37 this._started = false;
38 this.boundObserve = (...args) => this.observe(...args);
39 this.firstUpdateComplete = false;
40 this.fullyUpdated = false;
41 this.maxTabsLength = this.recentBrowsing
42 ? MAX_TABS_FOR_RECENT_BROWSING
44 this.recentlyClosedTabs = [];
45 this.searchQuery = "";
46 this.searchResults = null;
48 this.cumulativeSearches = 0;
52 ...ViewPage.properties,
53 searchResults: { type: Array },
54 showAll: { type: Boolean },
55 cumulativeSearches: { type: Number },
59 cardEl: "card-container",
60 emptyState: "fxview-empty-state",
61 searchTextbox: "fxview-search-textbox",
62 tabList: "fxview-tab-list",
65 observe(subject, topic) {
67 topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED ||
68 (topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH &&
69 subject.ownerGlobal == getWindow())
71 this.updateRecentlyClosedTabs();
81 this.updateRecentlyClosedTabs();
83 Services.obs.addObserver(
85 SS_NOTIFY_CLOSED_OBJECTS_CHANGED
87 Services.obs.addObserver(
89 SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH
92 if (this.recentBrowsing) {
93 this.recentBrowsingElement.addEventListener(
94 "fxview-search-textbox-query",
99 this.toggleVisibilityInCardContainer();
103 if (!this._started) {
106 this._started = false;
108 Services.obs.removeObserver(
110 SS_NOTIFY_CLOSED_OBJECTS_CHANGED
112 Services.obs.removeObserver(
114 SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH
117 if (this.recentBrowsing) {
118 this.recentBrowsingElement.removeEventListener(
119 "fxview-search-textbox-query",
124 this.toggleVisibilityInCardContainer();
127 disconnectedCallback() {
128 super.disconnectedCallback();
133 if (this.recentBrowsing && event.type === "fxview-search-textbox-query") {
134 this.onSearchQuery(event);
138 // We remove all the observers when the instance is not visible to the user
139 viewHiddenCallback() {
143 // We add observers and check for changes to the session store once the user return to this tab.
144 // or the instance becomes visible to the user
145 viewVisibleCallback() {
150 this.firstUpdateComplete = true;
153 getTabStateValue(tab, key) {
155 const tabEntries = tab.state.entries;
156 const activeIndex = tab.state.index - 1;
158 if (activeIndex >= 0 && tabEntries[activeIndex]) {
159 value = tabEntries[activeIndex][key];
165 updateRecentlyClosedTabs() {
166 let recentlyClosedTabsData =
167 lazy.SessionStore.getClosedTabData(getWindow());
168 if (Services.prefs.getBoolPref(INCLUDE_CLOSED_TABS_FROM_CLOSED_WINDOWS)) {
169 recentlyClosedTabsData.push(
170 ...lazy.SessionStore.getClosedTabDataFromClosedWindows()
173 // sort the aggregated list to most-recently-closed first
174 recentlyClosedTabsData.sort((a, b) => a.closedAt < b.closedAt);
175 this.recentlyClosedTabs = recentlyClosedTabsData;
176 this.normalizeRecentlyClosedData();
177 if (this.searchQuery) {
178 this.#updateSearchResults();
180 this.requestUpdate();
183 normalizeRecentlyClosedData() {
184 // Normalize data for fxview-tabs-list
185 this.recentlyClosedTabs.forEach(recentlyClosedItem => {
186 const targetURI = this.getTabStateValue(recentlyClosedItem, "url");
187 recentlyClosedItem.time = recentlyClosedItem.closedAt;
188 recentlyClosedItem.icon = recentlyClosedItem.image;
189 recentlyClosedItem.primaryL10nId = "fxviewtabrow-tabs-list-tab";
190 recentlyClosedItem.primaryL10nArgs = JSON.stringify({
191 targetURI: typeof targetURI === "string" ? targetURI : "",
193 recentlyClosedItem.secondaryL10nId =
194 "firefoxview-closed-tabs-dismiss-tab";
195 recentlyClosedItem.secondaryL10nArgs = JSON.stringify({
196 tabTitle: recentlyClosedItem.title,
198 recentlyClosedItem.url = targetURI;
203 const closedId = parseInt(e.originalTarget.closedId, 10);
204 const sourceClosedId = parseInt(e.originalTarget.sourceClosedId, 10);
205 if (isNaN(sourceClosedId)) {
206 lazy.SessionStore.undoCloseById(closedId, getWindow());
208 lazy.SessionStore.undoClosedTabFromClosedWindow(
216 let tabClosedAt = parseInt(e.originalTarget.time);
218 Array.from(this.tabList.rowEls).indexOf(e.originalTarget) + 1;
220 let now = Date.now();
221 let deltaSeconds = (now - tabClosedAt) / 1000;
222 Glean.firefoxviewNext.recentlyClosedTabs.record({
225 page: this.recentBrowsing ? "recentbrowsing" : "recentlyclosed",
227 if (this.searchQuery) {
229 .getKeyedHistogramById("FIREFOX_VIEW_CUMULATIVE_SEARCHES")
231 this.recentBrowsing ? "recentbrowsing" : "recentlyclosed",
232 this.cumulativeSearches
234 this.cumulativeSearches = 0;
239 const closedId = parseInt(e.originalTarget.closedId, 10);
240 const sourceClosedId = parseInt(e.originalTarget.sourceClosedId, 10);
241 const sourceWindowId = e.originalTarget.sourceWindowId;
242 if (!isNaN(sourceClosedId)) {
243 // the sourceClosedId is an identifier for a now-closed window the tab
245 lazy.SessionStore.forgetClosedTabById(closedId, {
248 } else if (sourceWindowId) {
249 // the sourceWindowId is an identifier for a currently-open window the tab
251 lazy.SessionStore.forgetClosedTabById(closedId, {
255 // without either identifier, SessionStore will need to walk its window collections
256 // to find the close tab with matching closedId
257 lazy.SessionStore.forgetClosedTabById(closedId);
261 let tabClosedAt = parseInt(e.originalTarget.time);
263 Array.from(this.tabList.rowEls).indexOf(e.originalTarget) + 1;
265 let now = Date.now();
266 let deltaSeconds = (now - tabClosedAt) / 1000;
267 Glean.firefoxviewNext.dismissClosedTabTabs.record({
270 page: this.recentBrowsing ? "recentbrowsing" : "recentlyclosed",
275 this.fullyUpdated = false;
279 this.fullyUpdated = true;
280 this.toggleVisibilityInCardContainer();
283 async scheduleUpdate() {
284 // Only defer initial update
285 if (!this.firstUpdateComplete) {
286 await new Promise(resolve => setTimeout(resolve));
288 super.scheduleUpdate();
291 emptyMessageTemplate() {
292 let descriptionHeader;
293 let descriptionLabels;
295 if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) {
296 // History pref set to never remember history
297 descriptionHeader = "firefoxview-dont-remember-history-empty-header-2";
298 descriptionLabels = [
299 "firefoxview-dont-remember-history-empty-description-one",
302 url: "about:preferences#privacy",
303 name: "history-settings-url-two",
306 descriptionHeader = "firefoxview-recentlyclosed-empty-header";
307 descriptionLabels = [
308 "firefoxview-recentlyclosed-empty-description",
309 "firefoxview-recentlyclosed-empty-description-two",
312 url: "about:firefoxview#history",
319 headerLabel=${descriptionHeader}
320 .descriptionLabels=${descriptionLabels}
321 .descriptionLink=${descriptionLink}
322 class="empty-state recentlyclosed"
323 ?isInnerCard=${this.recentBrowsing}
324 ?isSelectedTab=${this.selectedTab}
325 mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg"
327 </fxview-empty-state>
335 href="chrome://browser/content/firefoxview/firefoxview.css"
338 !this.recentBrowsing,
341 class="sticky-container bottom-fade"
342 ?hidden=${!this.selectedTab}
346 data-l10n-id="firefoxview-recently-closed-header"
349 <fxview-search-textbox
350 data-l10n-id="firefoxview-search-text-box-recentlyclosed"
351 data-l10n-attrs="placeholder"
352 @fxview-search-textbox-query=${this.onSearchQuery}
353 .size=${this.searchTextboxSize}
354 pageName=${this.recentBrowsing
357 ></fxview-search-textbox>
361 <div class=${classMap({ "cards-container": this.selectedTab })}>
363 shortPageName=${this.recentBrowsing ? "recentlyclosed" : null}
364 ?showViewAll=${this.recentBrowsing && this.recentlyClosedTabs.length}
365 ?preserveCollapseState=${this.recentBrowsing ? true : null}
366 ?hideHeader=${this.selectedTab}
367 ?hidden=${!this.recentlyClosedTabs.length && !this.recentBrowsing}
368 ?isEmptyState=${!this.recentlyClosedTabs.length}
372 data-l10n-id="firefoxview-recently-closed-header"
375 this.recentlyClosedTabs.length,
379 .maxTabsLength=${!this.recentBrowsing || this.showAll
381 : MAX_TABS_FOR_RECENT_BROWSING}
382 .searchQuery=${ifDefined(
383 this.searchResults && this.searchQuery
385 .tabItems=${this.searchResults || this.recentlyClosedTabs}
386 @fxview-tab-list-secondary-action=${this.onDismissTab}
387 @fxview-tab-list-primary-action=${this.onReopenTab}
388 secondaryActionClass="dismiss-button"
393 this.recentBrowsing && !this.recentlyClosedTabs.length,
394 () => html` <div slot="main">${this.emptyMessageTemplate()}</div> `
397 this.isShowAllLinkVisible(),
400 @click=${this.enableShowAll}
401 @keydown=${this.enableShowAll}
402 data-l10n-id="firefoxview-show-all"
403 ?hidden=${!this.isShowAllLinkVisible()}
411 this.selectedTab && !this.recentlyClosedTabs.length,
412 () => html` <div>${this.emptyMessageTemplate()}</div> `
419 this.searchQuery = e.detail.query;
420 this.showAll = false;
421 this.cumulativeSearches = this.searchQuery
422 ? this.cumulativeSearches + 1
424 this.#updateSearchResults();
427 #updateSearchResults() {
428 this.searchResults = this.searchQuery
429 ? searchTabList(this.searchQuery, this.recentlyClosedTabs)
433 isShowAllLinkVisible() {
435 this.recentBrowsing &&
437 this.searchResults.length > MAX_TABS_FOR_RECENT_BROWSING &&
442 enableShowAll(event) {
444 event.type == "click" ||
445 (event.type == "keydown" && event.code == "Enter") ||
446 (event.type == "keydown" && event.code == "Space")
448 event.preventDefault();
450 Glean.firefoxviewNext.searchShowAllShowallbutton.record({
451 section: "recentlyclosed",
456 customElements.define("view-recentlyclosed", RecentlyClosedTabsInView);