firefox-desktop/browser/components/firefoxview/history.mjs
2023-11-22 19:42:59 +01:00

640 lines
18 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/. */
import {
html,
ifDefined,
when,
} from "chrome://global/content/vendor/lit.all.mjs";
import { isSearchEnabled } from "./helpers.mjs";
import { ViewPage } from "./viewpage.mjs";
// eslint-disable-next-line import/no-unassigned-import
import "chrome://browser/content/migration/migration-wizard.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
FirefoxViewPlacesQuery:
"resource:///modules/firefox-view-places-query.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
});
let XPCOMUtils = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
).XPCOMUtils;
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"maxRowsPref",
"browser.firefox-view.max-history-rows",
-1
);
const NEVER_REMEMBER_HISTORY_PREF = "browser.privatebrowsing.autostart";
const HAS_IMPORTED_HISTORY_PREF = "browser.migrate.interactions.history";
const IMPORT_HISTORY_DISMISSED_PREF =
"browser.tabs.firefox-view.importHistory.dismissed";
const SEARCH_DEBOUNCE_RATE_MS = 500;
const SEARCH_DEBOUNCE_TIMEOUT_MS = 1000;
class HistoryInView extends ViewPage {
constructor() {
super();
this.allHistoryItems = new Map();
this.historyMapByDate = [];
this.historyMapBySite = [];
// Setting maxTabsLength to -1 for no max
this.maxTabsLength = -1;
this.placesQuery = new lazy.FirefoxViewPlacesQuery();
this.searchQuery = "";
this.searchResults = null;
this.sortOption = "date";
this.profileAge = 8;
this.fullyUpdated = false;
}
async connectedCallback() {
super.connectedCallback();
await this.updateHistoryData();
this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data));
XPCOMUtils.defineLazyPreferenceGetter(
this,
"importHistoryDismissedPref",
IMPORT_HISTORY_DISMISSED_PREF,
false,
() => {
this.requestUpdate();
}
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"hasImportedHistoryPref",
HAS_IMPORTED_HISTORY_PREF,
false,
() => {
this.requestUpdate();
}
);
if (!this.importHistoryDismissedPref && !this.hasImportedHistoryPrefs) {
let profileAccessor = await lazy.ProfileAge();
let profileCreateTime = await profileAccessor.created;
let timeNow = new Date().getTime();
let profileAge = timeNow - profileCreateTime;
// Convert milliseconds to days
this.profileAge = profileAge / 1000 / 60 / 60 / 24;
}
this.searchTask = new lazy.DeferredTask(
() => this.#updateSearchResults(),
SEARCH_DEBOUNCE_RATE_MS,
SEARCH_DEBOUNCE_TIMEOUT_MS
);
}
disconnectedCallback() {
super.disconnectedCallback();
this.placesQuery.close();
this.migrationWizardDialog?.removeEventListener(
"MigrationWizard:Close",
this.migrationWizardDialog
);
if (!this.searchTask.isFinalized) {
this.searchTask.finalize();
}
}
async #updateAllHistoryItems(allHistoryItems) {
if (allHistoryItems) {
this.allHistoryItems = allHistoryItems;
} else {
await this.updateHistoryData();
}
this.resetHistoryMaps();
this.lists.forEach(list => list.requestUpdate());
await this.#updateSearchResults();
}
async #updateSearchResults() {
if (this.searchQuery) {
try {
this.searchResults = await this.placesQuery.searchHistory(
this.searchQuery,
lazy.maxRowsPref
);
} catch (e) {
// Connection interrupted, ignore.
}
} else {
this.searchResults = null;
}
}
viewTabVisibleCallback() {
this.#updateAllHistoryItems();
this.placesQuery.observeHistory(data => this.#updateAllHistoryItems(data));
}
viewTabHiddenCallback() {
this.placesQuery.close();
}
static queries = {
cards: { all: "card-container:not([hidden])" },
migrationWizardDialog: "#migrationWizardDialog",
emptyState: "fxview-empty-state",
lists: { all: "fxview-tab-list" },
showAllHistoryBtn: ".show-all-history-button",
searchTextbox: "fxview-search-textbox",
sortInputs: { all: "input[name=history-sort-option]" },
panelList: "panel-list",
};
static properties = {
...ViewPage.properties,
allHistoryItems: { type: Map },
historyMapByDate: { type: Array },
historyMapBySite: { type: Array },
// Making profileAge a reactive property for testing
profileAge: { type: Number },
searchResults: { type: Array },
sortOption: { type: String },
};
async getUpdateComplete() {
await super.getUpdateComplete();
await Promise.all(Array.from(this.cards).map(card => card.updateComplete));
}
async updateHistoryData() {
this.allHistoryItems = await this.placesQuery.getHistory({
daysOld: 60,
limit: lazy.maxRowsPref,
sortBy: this.sortOption,
});
}
resetHistoryMaps() {
this.historyMapByDate = [];
this.historyMapBySite = [];
}
createHistoryMaps() {
if (this.sortOption === "date" && !this.historyMapByDate.length) {
const {
visitsFromToday,
visitsFromYesterday,
visitsByDay,
visitsByMonth,
} = this.placesQuery;
// Add visits from today and yesterday.
if (visitsFromToday.length) {
this.historyMapByDate.push({
l10nId: "firefoxview-history-date-today",
items: visitsFromToday,
});
}
if (visitsFromYesterday.length) {
this.historyMapByDate.push({
l10nId: "firefoxview-history-date-yesterday",
items: visitsFromYesterday,
});
}
// Add visits from this month, grouped by day.
visitsByDay.forEach(visits => {
this.historyMapByDate.push({
l10nId: "firefoxview-history-date-this-month",
items: visits,
});
});
// Add visits from previous months, grouped by month.
visitsByMonth.forEach(visits => {
this.historyMapByDate.push({
l10nId: "firefoxview-history-date-prev-month",
items: visits,
});
});
} else if (this.sortOption === "site" && !this.historyMapBySite.length) {
this.historyMapBySite = Array.from(
this.allHistoryItems.entries(),
([domain, items]) => ({
domain,
items,
l10nId: domain ? null : "firefoxview-history-site-localhost",
})
).sort((a, b) => a.domain.localeCompare(b.domain));
}
}
onPrimaryAction(e) {
// Record telemetry
Services.telemetry.recordEvent(
"firefoxview_next",
"history",
"visits",
null,
{}
);
let currentWindow = this.getWindow();
if (currentWindow.openTrustedLinkIn) {
let where = lazy.BrowserUtils.whereToOpenLink(
e.detail.originalEvent,
false,
true
);
if (where == "current") {
where = "tab";
}
currentWindow.openTrustedLinkIn(e.originalTarget.url, where);
}
}
onSecondaryAction(e) {
this.triggerNode = e.originalTarget;
e.target.querySelector("panel-list").toggle(e.detail.originalEvent);
}
deleteFromHistory(e) {
lazy.PlacesUtils.history.remove(this.triggerNode.url);
this.recordContextMenuTelemetry("delete-from-history", e);
}
async onChangeSortOption(e) {
this.sortOption = e.target.value;
Services.telemetry.recordEvent(
"firefoxview_next",
"sort_history",
"tabs",
null,
{
sort_type: this.sortOption,
}
);
await this.updateHistoryData();
await this.#updateSearchResults();
}
showAllHistory() {
// Record telemetry
Services.telemetry.recordEvent(
"firefoxview_next",
"show_all_history",
"tabs",
null,
{}
);
// Open History view in Library window
this.getWindow().PlacesCommandHook.showPlacesOrganizer("History");
}
async openMigrationWizard() {
let migrationWizardDialog = this.migrationWizardDialog;
if (migrationWizardDialog.open) {
return;
}
await customElements.whenDefined("migration-wizard");
// If we've been opened before, remove the old wizard and insert a
// new one to put it back into its starting state.
if (!migrationWizardDialog.firstElementChild) {
let wizard = document.createElement("migration-wizard");
wizard.toggleAttribute("dialog-mode", true);
migrationWizardDialog.appendChild(wizard);
}
migrationWizardDialog.firstElementChild.requestState();
this.migrationWizardDialog.addEventListener(
"MigrationWizard:Close",
function (e) {
e.currentTarget.close();
}
);
migrationWizardDialog.showModal();
}
shouldShowImportBanner() {
return (
this.profileAge < 8 &&
!this.hasImportedHistoryPref &&
!this.importHistoryDismissedPref
);
}
dismissImportHistory() {
Services.prefs.setBoolPref(IMPORT_HISTORY_DISMISSED_PREF, true);
}
updated() {
this.fullyUpdated = true;
}
panelListTemplate() {
return html`
<panel-list slot="menu" data-tab-type="history">
<panel-item
@click=${this.deleteFromHistory}
data-l10n-id="firefoxview-history-context-delete"
data-l10n-attrs="accesskey"
></panel-item>
<hr />
<panel-item
@click=${this.openInNewWindow}
data-l10n-id="fxviewtabrow-open-in-window"
data-l10n-attrs="accesskey"
></panel-item>
<panel-item
@click=${this.openInNewPrivateWindow}
data-l10n-id="fxviewtabrow-open-in-private-window"
data-l10n-attrs="accesskey"
></panel-item>
<hr />
<panel-item
@click=${this.copyLink}
data-l10n-id="fxviewtabrow-copy-link"
data-l10n-attrs="accesskey"
></panel-item>
</panel-list>
`;
}
/**
* The template to use for cards-container.
*/
get cardsTemplate() {
if (this.searchResults) {
return this.#searchResultsTemplate();
} else if (this.allHistoryItems.size) {
return this.#historyCardsTemplate();
}
return this.#emptyMessageTemplate();
}
#historyCardsTemplate() {
let cardsTemplate = [];
if (this.sortOption === "date" && this.historyMapByDate.length) {
this.historyMapByDate.forEach(historyItem => {
if (historyItem.items.length) {
let dateArg = JSON.stringify({ date: historyItem.items[0].time });
cardsTemplate.push(html`<card-container>
<h3
slot="header"
data-l10n-id=${historyItem.l10nId}
data-l10n-args=${dateArg}
></h3>
<fxview-tab-list
slot="main"
class="with-context-menu"
dateTimeFormat=${historyItem.l10nId.includes("prev-month")
? "dateTime"
: "time"}
hasPopup="menu"
maxTabsLength=${this.maxTabsLength}
.tabItems=${historyItem.items}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
${this.panelListTemplate()}
</fxview-tab-list>
</card-container>`);
}
});
} else if (this.historyMapBySite.length) {
this.historyMapBySite.forEach(historyItem => {
if (historyItem.items.length) {
cardsTemplate.push(html`<card-container>
<h3 slot="header" data-l10n-id="${ifDefined(historyItem.l10nId)}">
${historyItem.domain}
</h3>
<fxview-tab-list
slot="main"
class="with-context-menu"
dateTimeFormat="dateTime"
hasPopup="menu"
maxTabsLength=${this.maxTabsLength}
.tabItems=${historyItem.items}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
${this.panelListTemplate()}
</fxview-tab-list>
</card-container>`);
}
});
}
return cardsTemplate;
}
#emptyMessageTemplate() {
let descriptionHeader;
let descriptionLabels;
let descriptionLink;
if (Services.prefs.getBoolPref(NEVER_REMEMBER_HISTORY_PREF, false)) {
// History pref set to never remember history
descriptionHeader = "firefoxview-dont-remember-history-empty-header";
descriptionLabels = [
"firefoxview-dont-remember-history-empty-description",
"firefoxview-dont-remember-history-empty-description-two",
];
descriptionLink = {
url: "about:preferences#privacy",
name: "history-settings-url-two",
};
} else {
descriptionHeader = "firefoxview-history-empty-header";
descriptionLabels = [
"firefoxview-history-empty-description",
"firefoxview-history-empty-description-two",
];
descriptionLink = {
url: "about:preferences#privacy",
name: "history-settings-url",
};
}
return html`
<fxview-empty-state
headerLabel=${descriptionHeader}
.descriptionLabels=${descriptionLabels}
.descriptionLink=${descriptionLink}
class="empty-state history"
?isSelectedTab=${this.selectedTab}
mainImageUrl="chrome://browser/content/firefoxview/history-empty.svg"
>
</fxview-empty-state>
`;
}
#searchResultsTemplate() {
return html` <card-container toggleDisabled>
<h3
slot="header"
data-l10n-id="firefoxview-search-results-header"
data-l10n-args=${JSON.stringify({
query: this.#escapeHtmlEntities(this.searchQuery),
})}
></h3>
${when(
this.searchResults.length,
() =>
html`<h3
slot="secondary-header"
data-l10n-id="firefoxview-search-results-count"
data-l10n-args="${JSON.stringify({
count: this.searchResults.length,
})}"
></h3>`
)}
<fxview-tab-list
slot="main"
class="with-context-menu"
dateTimeFormat="dateTime"
hasPopup="menu"
maxTabsLength="-1"
.searchQuery=${this.searchQuery}
.tabItems=${this.searchResults}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
${this.panelListTemplate()}
</fxview-tab-list>
</card-container>`;
}
#escapeHtmlEntities(text) {
return (text || "")
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
render() {
if (!this.selectedTab) {
return null;
}
return html`
<link
rel="stylesheet"
href="chrome://browser/content/firefoxview/firefoxview-next.css"
/>
<link
rel="stylesheet"
href="chrome://browser/content/firefoxview/history.css"
/>
<dialog id="migrationWizardDialog"></dialog>
<div class="sticky-container bottom-fade">
<h2
class="page-header heading-large"
data-l10n-id="firefoxview-history-header"
></h2>
<div class="history-sort-options">
${when(
isSearchEnabled(),
() => html` <div class="history-sort-option">
<fxview-search-textbox
.query=${this.searchQuery}
data-l10n-id="firefoxview-search-text-box-history"
data-l10n-attrs="placeholder"
@fxview-search-textbox-query=${this.onSearchQuery}
></fxview-search-textbox>
</div>`
)}
<div class="history-sort-option">
<input
type="radio"
id="sort-by-date"
name="history-sort-option"
value="date"
?checked=${this.sortOption === "date"}
@click=${this.onChangeSortOption}
/>
<label
for="sort-by-date"
data-l10n-id="firefoxview-sort-history-by-date-label"
></label>
</div>
<div class="history-sort-option">
<input
type="radio"
id="sort-by-site"
name="history-sort-option"
value="site"
?checked=${this.sortOption === "site"}
@click=${this.onChangeSortOption}
/>
<label
for="sort-by-site"
data-l10n-id="firefoxview-sort-history-by-site-label"
></label>
</div>
</div>
</div>
<div class="cards-container">
<card-container
class="import-history-banner"
hideHeader="true"
?hidden=${!this.shouldShowImportBanner()}
>
<div slot="main">
<div class="banner-text">
<span data-l10n-id="firefoxview-import-history-header"></span>
<span
data-l10n-id="firefoxview-import-history-description"
></span>
</div>
<div class="buttons">
<button
class="primary choose-browser"
data-l10n-id="firefoxview-choose-browser-button"
@click=${this.openMigrationWizard}
></button>
<button
class="close ghost-button"
data-l10n-id="firefoxview-import-history-close-button"
@click=${this.dismissImportHistory}
></button>
</div>
</div>
</card-container>
${this.cardsTemplate}
</div>
<div
class="show-all-history-footer"
?hidden=${!this.allHistoryItems.size}
>
<button
class="show-all-history-button"
data-l10n-id="firefoxview-show-all-history"
@click=${this.showAllHistory}
?hidden=${this.searchResults}
></button>
</div>
`;
}
async onSearchQuery(e) {
this.searchQuery = e.detail.query;
this.searchTask.arm();
}
willUpdate(changedProperties) {
this.fullyUpdated = false;
if (this.allHistoryItems.size && !changedProperties.has("sortOption")) {
// onChangeSortOption() will update history data once it has been fetched
// from the API.
this.createHistoryMaps();
}
}
}
customElements.define("view-history", HistoryInView);