history.mjs (15436B)
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 html, 7 ifDefined, 8 when, 9 } from "chrome://global/content/vendor/lit.all.mjs"; 10 import { escapeHtmlEntities, navigateToLink } from "./helpers.mjs"; 11 import { ViewPage } from "./viewpage.mjs"; 12 // eslint-disable-next-line import/no-unassigned-import 13 import "chrome://browser/content/migration/migration-wizard.mjs"; 14 // eslint-disable-next-line import/no-unassigned-import 15 import "chrome://global/content/elements/moz-button.mjs"; 16 17 const lazy = {}; 18 19 ChromeUtils.defineESModuleGetters(lazy, { 20 HistoryController: "resource:///modules/HistoryController.sys.mjs", 21 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 22 ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs", 23 }); 24 25 let XPCOMUtils = ChromeUtils.importESModule( 26 "resource://gre/modules/XPCOMUtils.sys.mjs" 27 ).XPCOMUtils; 28 29 const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart"; 30 const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history"; 31 const IMPORT_HISTORY_DISMISSED_PREF = 32 "browser.tabs.firefox-view.importHistory.dismissed"; 33 34 const SEARCH_RESULTS_LIMIT = 300; 35 36 class HistoryInView extends ViewPage { 37 constructor() { 38 super(); 39 this._started = false; 40 // Setting maxTabsLength to -1 for no max 41 this.maxTabsLength = -1; 42 this.profileAge = 8; 43 this.fullyUpdated = false; 44 this.cumulativeSearches = 0; 45 } 46 47 controller = new lazy.HistoryController(this, { 48 searchResultsLimit: SEARCH_RESULTS_LIMIT, 49 }); 50 51 start() { 52 if (this._started) { 53 return; 54 } 55 this._started = true; 56 57 this.controller.updateCache(); 58 59 this.toggleVisibilityInCardContainer(); 60 } 61 62 async connectedCallback() { 63 super.connectedCallback(); 64 XPCOMUtils.defineLazyPreferenceGetter( 65 this, 66 "importHistoryDismissedPref", 67 IMPORT_HISTORY_DISMISSED_PREF, 68 false, 69 () => { 70 this.requestUpdate(); 71 } 72 ); 73 XPCOMUtils.defineLazyPreferenceGetter( 74 this, 75 "hasImportedHistoryPref", 76 HAS_IMPORTED_HISTORY_PREF, 77 false, 78 () => { 79 this.requestUpdate(); 80 } 81 ); 82 83 if (!this.importHistoryDismissedPref && !this.hasImportedHistoryPrefs) { 84 let profileAccessor = await lazy.ProfileAge(); 85 let profileCreateTime = await profileAccessor.created; 86 let timeNow = new Date().getTime(); 87 let profileAge = timeNow - profileCreateTime; 88 // Convert milliseconds to days 89 this.profileAge = profileAge / 1000 / 60 / 60 / 24; 90 } 91 } 92 93 stop() { 94 if (!this._started) { 95 return; 96 } 97 this._started = false; 98 99 this.toggleVisibilityInCardContainer(); 100 } 101 102 disconnectedCallback() { 103 super.disconnectedCallback(); 104 this.stop(); 105 this.migrationWizardDialog?.removeEventListener( 106 "MigrationWizard:Close", 107 this.migrationWizardDialog 108 ); 109 } 110 111 viewVisibleCallback() { 112 this.start(); 113 } 114 115 viewHiddenCallback() { 116 this.stop(); 117 } 118 119 static queries = { 120 cards: { all: "card-container:not([hidden])" }, 121 migrationWizardDialog: "#migrationWizardDialog", 122 emptyState: "fxview-empty-state", 123 lists: { all: "fxview-tab-list" }, 124 showAllHistoryBtn: ".show-all-history-button", 125 searchTextbox: "moz-input-search", 126 sortInputs: { all: "input[name=history-sort-option]" }, 127 panelList: "panel-list", 128 }; 129 130 static properties = { 131 // Making profileAge a reactive property for testing 132 profileAge: { type: Number }, 133 }; 134 135 async getUpdateComplete() { 136 await super.getUpdateComplete(); 137 await Promise.all(Array.from(this.cards).map(card => card.updateComplete)); 138 } 139 140 onPrimaryAction(e) { 141 navigateToLink(e); 142 // Record telemetry 143 Glean.firefoxviewNext.historyVisits.record(); 144 145 if (this.controller.searchQuery) { 146 Glean.firefoxview.cumulativeSearches.history.accumulateSingleSample( 147 this.cumulativeSearches 148 ); 149 this.cumulativeSearches = 0; 150 } 151 } 152 153 onSecondaryAction(e) { 154 this.triggerNode = e.originalTarget; 155 this.panelList.toggle(e.detail.originalEvent); 156 } 157 158 deleteFromHistory(e) { 159 this.controller.deleteFromHistory().catch(console.error); 160 this.recordContextMenuTelemetry("delete-from-history", e); 161 } 162 163 onChangeSortOption(e) { 164 this.controller.onChangeSortOption(e); 165 Glean.firefoxviewNext.sortHistoryTabs.record({ 166 sort_type: this.controller.sortOption, 167 search_start: this.controller.searchQuery ? "true" : "false", 168 }); 169 } 170 171 onSearchQuery(e) { 172 if (!this.recentBrowsing) { 173 Glean.firefoxviewNext.searchInitiatedSearch.record({ 174 page: "history", 175 }); 176 } 177 this.controller.onSearchQuery(e); 178 this.cumulativeSearches = this.controller.searchQuery 179 ? this.cumulativeSearches + 1 180 : 0; 181 } 182 183 showAllHistory() { 184 // Record telemetry 185 Glean.firefoxviewNext.showAllHistoryTabs.record(); 186 187 // Open History view in Library window 188 this.getWindow().PlacesCommandHook.showPlacesOrganizer("History"); 189 } 190 191 async openMigrationWizard() { 192 let migrationWizardDialog = this.migrationWizardDialog; 193 194 if (migrationWizardDialog.open) { 195 return; 196 } 197 198 await customElements.whenDefined("migration-wizard"); 199 200 // If we've been opened before, remove the old wizard and insert a 201 // new one to put it back into its starting state. 202 if (!migrationWizardDialog.firstElementChild) { 203 let wizard = document.createElement("migration-wizard"); 204 wizard.toggleAttribute("dialog-mode", true); 205 migrationWizardDialog.appendChild(wizard); 206 } 207 migrationWizardDialog.firstElementChild.requestState(); 208 209 this.migrationWizardDialog.addEventListener( 210 "MigrationWizard:Close", 211 function (e) { 212 e.currentTarget.close(); 213 } 214 ); 215 216 migrationWizardDialog.showModal(); 217 } 218 219 shouldShowImportBanner() { 220 return ( 221 this.profileAge < 8 && 222 !this.hasImportedHistoryPref && 223 !this.importHistoryDismissedPref && 224 Services.policies.isAllowed("profileImport") 225 ); 226 } 227 228 dismissImportHistory() { 229 Services.prefs.setBoolPref(IMPORT_HISTORY_DISMISSED_PREF, true); 230 } 231 232 updated() { 233 this.fullyUpdated = true; 234 if (this.lists?.length) { 235 this.toggleVisibilityInCardContainer(); 236 } 237 } 238 239 panelListTemplate() { 240 return html` 241 <panel-list slot="menu" data-tab-type="history"> 242 <panel-item 243 @click=${this.deleteFromHistory} 244 data-l10n-id="firefoxview-history-context-delete" 245 data-l10n-attrs="accesskey" 246 ></panel-item> 247 <hr /> 248 <panel-item 249 @click=${this.openInNewWindow} 250 data-l10n-id="fxviewtabrow-open-in-window" 251 data-l10n-attrs="accesskey" 252 ></panel-item> 253 <panel-item 254 @click=${this.openInNewPrivateWindow} 255 data-l10n-id="fxviewtabrow-open-in-private-window" 256 data-l10n-attrs="accesskey" 257 ?hidden=${!lazy.PrivateBrowsingUtils.enabled} 258 ></panel-item> 259 <hr /> 260 <panel-item 261 @click=${this.copyLink} 262 data-l10n-id="fxviewtabrow-copy-link" 263 data-l10n-attrs="accesskey" 264 ></panel-item> 265 </panel-list> 266 `; 267 } 268 269 /** 270 * The template to use for cards-container. 271 */ 272 get cardsTemplate() { 273 if (this.controller.searchResults) { 274 return this.#searchResultsTemplate(); 275 } else if (!this.controller.isHistoryEmpty) { 276 return this.#historyCardsTemplate(); 277 } 278 return this.#emptyMessageTemplate(); 279 } 280 281 #historyCardsTemplate() { 282 let cardsTemplate = []; 283 switch (this.controller.sortOption) { 284 case "date": 285 cardsTemplate = this.controller.historyVisits.map(historyItem => { 286 let dateArg = JSON.stringify({ date: historyItem.items[0].time }); 287 return html`<card-container> 288 <h3 289 slot="header" 290 data-l10n-id=${historyItem.l10nId} 291 data-l10n-args=${dateArg} 292 ></h3> 293 <fxview-tab-list 294 slot="main" 295 secondaryActionClass="options-button" 296 dateTimeFormat=${historyItem.l10nId.includes("prev-month") 297 ? "dateTime" 298 : "time"} 299 hasPopup="menu" 300 maxTabsLength=${this.maxTabsLength} 301 .tabItems=${historyItem.items} 302 @fxview-tab-list-primary-action=${this.onPrimaryAction} 303 @fxview-tab-list-secondary-action=${this.onSecondaryAction} 304 > 305 </fxview-tab-list> 306 </card-container>`; 307 }); 308 break; 309 case "site": 310 cardsTemplate = this.controller.historyVisits.map(historyItem => { 311 return html`<card-container> 312 <h3 slot="header" data-l10n-id=${ifDefined(historyItem.l10nId)}> 313 ${historyItem.domain} 314 </h3> 315 <fxview-tab-list 316 slot="main" 317 secondaryActionClass="options-button" 318 dateTimeFormat="dateTime" 319 hasPopup="menu" 320 maxTabsLength=${this.maxTabsLength} 321 .tabItems=${historyItem.items} 322 @fxview-tab-list-primary-action=${this.onPrimaryAction} 323 @fxview-tab-list-secondary-action=${this.onSecondaryAction} 324 > 325 </fxview-tab-list> 326 </card-container>`; 327 }); 328 break; 329 } 330 return cardsTemplate; 331 } 332 333 #emptyMessageTemplate() { 334 let descriptionHeader; 335 let descriptionLabels; 336 let descriptionLink; 337 if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) { 338 // History pref set to never remember history 339 descriptionHeader = "firefoxview-dont-remember-history-empty-header-2"; 340 descriptionLabels = [ 341 "firefoxview-dont-remember-history-empty-description-one", 342 ]; 343 descriptionLink = { 344 url: "about:preferences#privacy", 345 name: "history-settings-url-two", 346 }; 347 } else { 348 descriptionHeader = "firefoxview-history-empty-header"; 349 descriptionLabels = [ 350 "firefoxview-history-empty-description", 351 "firefoxview-history-empty-description-two", 352 ]; 353 descriptionLink = { 354 url: "about:preferences#privacy", 355 name: "history-settings-url", 356 }; 357 } 358 return html` 359 <fxview-empty-state 360 headerLabel=${descriptionHeader} 361 .descriptionLabels=${descriptionLabels} 362 .descriptionLink=${descriptionLink} 363 class="empty-state history" 364 ?isSelectedTab=${this.selectedTab} 365 mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg" 366 > 367 </fxview-empty-state> 368 `; 369 } 370 371 #searchResultsTemplate() { 372 return html` <card-container toggleDisabled> 373 <h3 374 slot="header" 375 data-l10n-id="firefoxview-search-results-header" 376 data-l10n-args=${JSON.stringify({ 377 query: escapeHtmlEntities(this.controller.searchQuery), 378 })} 379 ></h3> 380 ${when( 381 this.controller.searchResults.length, 382 () => 383 html`<h3 384 slot="secondary-header" 385 data-l10n-id="firefoxview-search-results-count" 386 data-l10n-args=${JSON.stringify({ 387 count: this.controller.searchResults.length, 388 })} 389 ></h3>` 390 )} 391 <fxview-tab-list 392 slot="main" 393 secondaryActionClass="options-button" 394 dateTimeFormat="dateTime" 395 hasPopup="menu" 396 maxTabsLength="-1" 397 .searchQuery=${this.controller.searchQuery} 398 .tabItems=${this.controller.searchResults} 399 @fxview-tab-list-primary-action=${this.onPrimaryAction} 400 @fxview-tab-list-secondary-action=${this.onSecondaryAction} 401 > 402 </fxview-tab-list> 403 </card-container>`; 404 } 405 406 render() { 407 if (!this.selectedTab) { 408 return null; 409 } 410 return html` 411 <link 412 rel="stylesheet" 413 href="chrome://browser/content/firefoxview/firefoxview.css" 414 /> 415 <link 416 rel="stylesheet" 417 href="chrome://browser/content/firefoxview/history.css" 418 /> 419 <dialog id="migrationWizardDialog"></dialog> 420 ${this.panelListTemplate()} 421 <div class="sticky-container bottom-fade"> 422 <h2 class="page-header" data-l10n-id="firefoxview-history-header"></h2> 423 <div class="history-sort-options"> 424 <div class="history-sort-option"> 425 <moz-input-search 426 data-l10n-id="firefoxview-search-text-box-history" 427 data-l10n-attrs="placeholder" 428 @MozInputSearch:search=${this.onSearchQuery} 429 ></moz-input-search> 430 </div> 431 <div class="history-sort-option"> 432 <input 433 type="radio" 434 id="sort-by-date" 435 name="history-sort-option" 436 value="date" 437 ?checked=${this.controller.sortOption === "date"} 438 @click=${this.onChangeSortOption} 439 /> 440 <label 441 for="sort-by-date" 442 data-l10n-id="firefoxview-sort-history-by-date-label" 443 ></label> 444 </div> 445 <div class="history-sort-option"> 446 <input 447 type="radio" 448 id="sort-by-site" 449 name="history-sort-option" 450 value="site" 451 ?checked=${this.controller.sortOption === "site"} 452 @click=${this.onChangeSortOption} 453 /> 454 <label 455 for="sort-by-site" 456 data-l10n-id="firefoxview-sort-history-by-site-label" 457 ></label> 458 </div> 459 </div> 460 </div> 461 <div class="cards-container"> 462 <card-container 463 class="import-history-banner" 464 hideHeader="true" 465 ?hidden=${!this.shouldShowImportBanner()} 466 role="group" 467 aria-labelledby="header" 468 aria-describedby="description" 469 > 470 <div slot="main"> 471 <div class="banner-text"> 472 <span 473 data-l10n-id="firefoxview-import-history-header" 474 id="header" 475 ></span> 476 <span 477 data-l10n-id="firefoxview-import-history-description" 478 id="description" 479 ></span> 480 </div> 481 <div class="buttons"> 482 <button 483 class="primary choose-browser" 484 data-l10n-id="firefoxview-choose-browser-button" 485 @click=${this.openMigrationWizard} 486 ></button> 487 <moz-button 488 class="close" 489 type="icon ghost" 490 data-l10n-id="firefoxview-import-history-close-button" 491 @click=${this.dismissImportHistory} 492 ></moz-button> 493 </div> 494 </div> 495 </card-container> 496 ${this.cardsTemplate} 497 </div> 498 <div 499 class="show-all-history-footer" 500 ?hidden=${this.controller.isHistoryEmpty} 501 > 502 <button 503 class="show-all-history-button" 504 data-l10n-id="firefoxview-show-all-history" 505 @click=${this.showAllHistory} 506 ?hidden=${this.controller.searchResults} 507 ></button> 508 </div> 509 `; 510 } 511 512 willUpdate() { 513 this.fullyUpdated = false; 514 } 515 } 516 customElements.define("view-history", HistoryInView);