recentlyclosed.mjs (13839B)
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/. */ 4 5 import { 6 classMap, 7 html, 8 ifDefined, 9 when, 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"; 18 19 const lazy = {}; 20 ChromeUtils.defineESModuleGetters(lazy, { 21 SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", 22 }); 23 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"; 29 30 function getWindow() { 31 return window.browsingContext.embedderWindowGlobal.browsingContext.window; 32 } 33 34 class RecentlyClosedTabsInView extends ViewPage { 35 constructor() { 36 super(); 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 43 : -1; 44 this.recentlyClosedTabs = []; 45 this.searchQuery = ""; 46 this.searchResults = null; 47 this.showAll = false; 48 this.cumulativeSearches = 0; 49 } 50 51 static properties = { 52 ...ViewPage.properties, 53 searchResults: { type: Array }, 54 showAll: { type: Boolean }, 55 cumulativeSearches: { type: Number }, 56 }; 57 58 static queries = { 59 cardEl: "card-container", 60 emptyState: "fxview-empty-state", 61 searchTextbox: "moz-input-search", 62 tabList: "fxview-tab-list", 63 }; 64 65 observe(subject, topic) { 66 if ( 67 topic == SS_NOTIFY_CLOSED_OBJECTS_CHANGED || 68 (topic == SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH && 69 subject.ownerGlobal == getWindow()) 70 ) { 71 this.updateRecentlyClosedTabs(); 72 } 73 } 74 75 start() { 76 if (this._started) { 77 return; 78 } 79 this._started = true; 80 this.paused = false; 81 this.updateRecentlyClosedTabs(); 82 83 Services.obs.addObserver( 84 this.boundObserve, 85 SS_NOTIFY_CLOSED_OBJECTS_CHANGED 86 ); 87 Services.obs.addObserver( 88 this.boundObserve, 89 SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH 90 ); 91 92 if (this.recentBrowsing) { 93 this.recentBrowsingElement.addEventListener( 94 "MozInputSearch:search", 95 this 96 ); 97 } 98 99 this.toggleVisibilityInCardContainer(); 100 } 101 102 stop() { 103 if (!this._started) { 104 return; 105 } 106 this._started = false; 107 108 Services.obs.removeObserver( 109 this.boundObserve, 110 SS_NOTIFY_CLOSED_OBJECTS_CHANGED 111 ); 112 Services.obs.removeObserver( 113 this.boundObserve, 114 SS_NOTIFY_BROWSER_SHUTDOWN_FLUSH 115 ); 116 117 if (this.recentBrowsing) { 118 this.recentBrowsingElement.removeEventListener( 119 "MozInputSearch:search", 120 this 121 ); 122 } 123 124 this.toggleVisibilityInCardContainer(); 125 } 126 127 disconnectedCallback() { 128 super.disconnectedCallback(); 129 this.stop(); 130 } 131 132 handleEvent(event) { 133 if (this.recentBrowsing && event.type === "MozInputSearch:search") { 134 this.onSearchQuery(event); 135 } 136 } 137 138 // We remove all the observers when the instance is not visible to the user 139 viewHiddenCallback() { 140 this.stop(); 141 } 142 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() { 146 this.start(); 147 } 148 149 firstUpdated() { 150 this.firstUpdateComplete = true; 151 } 152 153 getTabStateValue(tab, key) { 154 let value = ""; 155 const tabEntries = tab.state.entries; 156 const activeIndex = tab.state.index - 1; 157 158 if (activeIndex >= 0 && tabEntries[activeIndex]) { 159 value = tabEntries[activeIndex][key]; 160 } 161 162 return value; 163 } 164 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() 171 ); 172 } 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(); 179 } 180 this.requestUpdate(); 181 } 182 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 : "", 192 }); 193 recentlyClosedItem.secondaryL10nId = 194 "firefoxview-closed-tabs-dismiss-tab"; 195 recentlyClosedItem.secondaryL10nArgs = JSON.stringify({ 196 tabTitle: recentlyClosedItem.title, 197 }); 198 recentlyClosedItem.url = targetURI; 199 }); 200 } 201 202 onReopenTab(e) { 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()); 207 } else { 208 lazy.SessionStore.undoClosedTabFromClosedWindow( 209 { sourceClosedId }, 210 closedId, 211 getWindow() 212 ); 213 } 214 215 // Record telemetry 216 let tabClosedAt = parseInt(e.originalTarget.time); 217 const position = 218 Array.from(this.tabList.rowEls).indexOf(e.originalTarget) + 1; 219 220 let now = Date.now(); 221 let deltaSeconds = (now - tabClosedAt) / 1000; 222 Glean.firefoxviewNext.recentlyClosedTabs.record({ 223 position, 224 delta: deltaSeconds, 225 page: this.recentBrowsing ? "recentbrowsing" : "recentlyclosed", 226 }); 227 if (this.searchQuery) { 228 Glean.firefoxview.cumulativeSearches[ 229 this.recentBrowsing ? "recentbrowsing" : "recentlyclosed" 230 ].accumulateSingleSample(this.cumulativeSearches); 231 this.cumulativeSearches = 0; 232 } 233 } 234 235 onDismissTab(e) { 236 const closedId = parseInt(e.originalTarget.closedId, 10); 237 const sourceClosedId = parseInt(e.originalTarget.sourceClosedId, 10); 238 const sourceWindowId = e.originalTarget.sourceWindowId; 239 if (!isNaN(sourceClosedId)) { 240 // the sourceClosedId is an identifier for a now-closed window the tab 241 // was closed in. 242 lazy.SessionStore.forgetClosedTabById(closedId, { 243 sourceClosedId, 244 }); 245 } else if (sourceWindowId) { 246 // the sourceWindowId is an identifier for a currently-open window the tab 247 // was closed in. 248 lazy.SessionStore.forgetClosedTabById(closedId, { 249 sourceWindowId, 250 }); 251 } else { 252 // without either identifier, SessionStore will need to walk its window collections 253 // to find the close tab with matching closedId 254 lazy.SessionStore.forgetClosedTabById(closedId); 255 } 256 257 // Record telemetry 258 let tabClosedAt = parseInt(e.originalTarget.time); 259 const position = 260 Array.from(this.tabList.rowEls).indexOf(e.originalTarget) + 1; 261 262 let now = Date.now(); 263 let deltaSeconds = (now - tabClosedAt) / 1000; 264 Glean.firefoxviewNext.dismissClosedTabTabs.record({ 265 position, 266 delta: deltaSeconds, 267 page: this.recentBrowsing ? "recentbrowsing" : "recentlyclosed", 268 }); 269 } 270 271 willUpdate() { 272 this.fullyUpdated = false; 273 } 274 275 updated() { 276 this.fullyUpdated = true; 277 this.toggleVisibilityInCardContainer(); 278 } 279 280 async scheduleUpdate() { 281 // Only defer initial update 282 if (!this.firstUpdateComplete) { 283 await new Promise(resolve => setTimeout(resolve)); 284 } 285 super.scheduleUpdate(); 286 } 287 288 emptyMessageTemplate() { 289 let descriptionHeader; 290 let descriptionLabels; 291 let descriptionLink; 292 if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) { 293 // History pref set to never remember history 294 descriptionHeader = "firefoxview-dont-remember-history-empty-header-2"; 295 descriptionLabels = [ 296 "firefoxview-dont-remember-history-empty-description-one", 297 ]; 298 descriptionLink = { 299 url: "about:preferences#privacy", 300 name: "history-settings-url-two", 301 }; 302 } else { 303 descriptionHeader = "firefoxview-recentlyclosed-empty-header"; 304 descriptionLabels = [ 305 "firefoxview-recentlyclosed-empty-description", 306 "firefoxview-recentlyclosed-empty-description-two", 307 ]; 308 descriptionLink = { 309 url: "about:firefoxview#history", 310 name: "history-url", 311 sameTarget: "true", 312 }; 313 } 314 return html` 315 <fxview-empty-state 316 headerLabel=${descriptionHeader} 317 .descriptionLabels=${descriptionLabels} 318 .descriptionLink=${descriptionLink} 319 class="empty-state recentlyclosed" 320 ?isInnerCard=${this.recentBrowsing} 321 ?isSelectedTab=${this.selectedTab} 322 mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg" 323 > 324 </fxview-empty-state> 325 `; 326 } 327 328 render() { 329 return html` 330 <link 331 rel="stylesheet" 332 href="chrome://browser/content/firefoxview/firefoxview.css" 333 /> 334 ${when( 335 !this.recentBrowsing, 336 () => 337 html`<div 338 class="sticky-container bottom-fade" 339 ?hidden=${!this.selectedTab} 340 > 341 <h2 342 class="page-header" 343 data-l10n-id="firefoxview-recently-closed-header" 344 ></h2> 345 <div> 346 <moz-input-search 347 data-l10n-id="firefoxview-search-text-box-recentlyclosed" 348 data-l10n-attrs="placeholder" 349 @MozInputSearch:search=${this.onSearchQuery} 350 ></moz-input-search> 351 </div> 352 </div>` 353 )} 354 <div class=${classMap({ "cards-container": this.selectedTab })}> 355 <card-container 356 shortPageName=${this.recentBrowsing ? "recentlyclosed" : null} 357 ?showViewAll=${this.recentBrowsing && this.recentlyClosedTabs.length} 358 ?preserveCollapseState=${this.recentBrowsing ? true : null} 359 ?hideHeader=${this.selectedTab} 360 ?hidden=${!this.recentlyClosedTabs.length && !this.recentBrowsing} 361 ?isEmptyState=${!this.recentlyClosedTabs.length} 362 > 363 <h3 364 slot="header" 365 data-l10n-id="firefoxview-recently-closed-header" 366 ></h3> 367 ${when( 368 this.recentlyClosedTabs.length, 369 () => html` 370 <fxview-tab-list 371 slot="main" 372 .maxTabsLength=${!this.recentBrowsing || this.showAll 373 ? -1 374 : MAX_TABS_FOR_RECENT_BROWSING} 375 .searchQuery=${ifDefined( 376 this.searchResults && this.searchQuery 377 )} 378 .tabItems=${this.searchResults || this.recentlyClosedTabs} 379 @fxview-tab-list-secondary-action=${this.onDismissTab} 380 @fxview-tab-list-primary-action=${this.onReopenTab} 381 secondaryActionClass="dismiss-button" 382 ></fxview-tab-list> 383 ` 384 )} 385 ${when( 386 this.recentBrowsing && !this.recentlyClosedTabs.length, 387 () => html` <div slot="main">${this.emptyMessageTemplate()}</div> ` 388 )} 389 ${when( 390 this.isShowAllLinkVisible(), 391 () => 392 html` <div 393 @click=${this.enableShowAll} 394 @keydown=${this.enableShowAll} 395 data-l10n-id="firefoxview-show-all" 396 ?hidden=${!this.isShowAllLinkVisible()} 397 slot="footer" 398 tabindex="0" 399 role="link" 400 ></div>` 401 )} 402 </card-container> 403 ${when( 404 this.selectedTab && !this.recentlyClosedTabs.length, 405 () => html` <div>${this.emptyMessageTemplate()}</div> ` 406 )} 407 </div> 408 `; 409 } 410 411 onSearchQuery(e) { 412 if (!this.recentBrowsing) { 413 Glean.firefoxviewNext.searchInitiatedSearch.record({ 414 page: "recentlyclosed", 415 }); 416 } 417 this.searchQuery = e.detail.query; 418 this.showAll = false; 419 this.cumulativeSearches = this.searchQuery 420 ? this.cumulativeSearches + 1 421 : 0; 422 this.#updateSearchResults(); 423 } 424 425 #updateSearchResults() { 426 this.searchResults = this.searchQuery 427 ? searchTabList(this.searchQuery, this.recentlyClosedTabs) 428 : null; 429 } 430 431 isShowAllLinkVisible() { 432 return ( 433 this.recentBrowsing && 434 this.searchQuery && 435 this.searchResults.length > MAX_TABS_FOR_RECENT_BROWSING && 436 !this.showAll 437 ); 438 } 439 440 enableShowAll(event) { 441 if ( 442 event.type == "click" || 443 (event.type == "keydown" && event.code == "Enter") || 444 (event.type == "keydown" && event.code == "Space") 445 ) { 446 event.preventDefault(); 447 this.showAll = true; 448 Glean.firefoxviewNext.searchShowAllShowallbutton.record({ 449 section: "recentlyclosed", 450 }); 451 } 452 } 453 } 454 customElements.define("view-recentlyclosed", RecentlyClosedTabsInView);