diff options
Diffstat (limited to 'browser/components/firefoxview/HistoryController.sys.mjs')
-rw-r--r-- | browser/components/firefoxview/HistoryController.sys.mjs | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/browser/components/firefoxview/HistoryController.sys.mjs b/browser/components/firefoxview/HistoryController.sys.mjs new file mode 100644 index 0000000000..b6f316e8e7 --- /dev/null +++ b/browser/components/firefoxview/HistoryController.sys.mjs @@ -0,0 +1,383 @@ +/* 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 = {}; + +import { getLogger } from "chrome://browser/content/firefoxview/helpers.mjs"; + +ChromeUtils.defineESModuleGetters(lazy, { + PlacesQuery: "resource://gre/modules/PlacesQuery.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.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 HISTORY_MAP_L10N_IDS = { + sidebar: { + "history-date-today": "sidebar-history-date-today", + "history-date-yesterday": "sidebar-history-date-yesterday", + "history-date-this-month": "sidebar-history-date-this-month", + "history-date-prev-month": "sidebar-history-date-prev-month", + }, + firefoxview: { + "history-date-today": "firefoxview-history-date-today", + "history-date-yesterday": "firefoxview-history-date-yesterday", + "history-date-this-month": "firefoxview-history-date-this-month", + "history-date-prev-month": "firefoxview-history-date-prev-month", + }, +}; + +/** + * A list of visits displayed on a card. + * + * @typedef {object} CardEntry + * + * @property {string} domain + * @property {HistoryVisit[]} items + * @property {string} l10nId + */ + +export class HistoryController { + /** + * @type {{ entries: CardEntry[]; searchQuery: string; sortOption: string; }} + */ + historyCache; + host; + searchQuery; + sortOption; + #todaysDate; + #yesterdaysDate; + + constructor(host, options) { + this.placesQuery = new lazy.PlacesQuery(); + this.searchQuery = ""; + this.sortOption = "date"; + this.searchResultsLimit = options?.searchResultsLimit || 300; + this.component = HISTORY_MAP_L10N_IDS?.[options?.component] + ? options?.component + : "firefoxview"; + this.historyCache = { + entries: [], + searchQuery: null, + sortOption: null, + }; + this.host = host; + + host.addController(this); + } + + hostConnected() { + this.placesQuery.observeHistory(historyMap => this.updateCache(historyMap)); + } + + hostDisconnected() { + ChromeUtils.idleDispatch(() => this.placesQuery.close()); + } + + deleteFromHistory() { + lazy.PlacesUtils.history.remove(this.host.triggerNode.url); + } + + onSearchQuery(e) { + this.searchQuery = e.detail.query; + this.updateCache(); + } + + onChangeSortOption(e) { + this.sortOption = e.target.value; + this.updateCache(); + } + + get historyVisits() { + return this.historyCache.entries; + } + + get searchResults() { + return this.historyCache.searchQuery + ? this.historyCache.entries[0].items + : null; + } + + get totalVisitsCount() { + return this.historyVisits.reduce( + (count, entry) => count + entry.items.length, + 0 + ); + } + + get isHistoryEmpty() { + return !this.historyVisits.length; + } + + /** + * Update cached history. + * + * @param {Map<CacheKey, HistoryVisit[]>} [historyMap] + * If provided, performs an update using the given data (instead of fetching + * it from the db). + */ + async updateCache(historyMap) { + const { searchQuery, sortOption } = this; + const entries = searchQuery + ? await this.#getVisitsForSearchQuery(searchQuery) + : await this.#getVisitsForSortOption(sortOption, historyMap); + if (this.searchQuery !== searchQuery || this.sortOption !== sortOption) { + // This query is stale, discard results and do not update the cache / UI. + return; + } + for (const { items } of entries) { + for (const item of items) { + this.#normalizeVisit(item); + } + } + this.historyCache = { entries, searchQuery, sortOption }; + this.host.requestUpdate(); + } + + /** + * Normalize data for fxview-tabs-list. + * + * @param {HistoryVisit} visit + * The visit to format. + */ + #normalizeVisit(visit) { + visit.time = visit.date.getTime(); + visit.title = visit.title || visit.url; + visit.icon = `page-icon:${visit.url}`; + visit.primaryL10nId = "fxviewtabrow-tabs-list-tab"; + visit.primaryL10nArgs = JSON.stringify({ + targetURI: visit.url, + }); + visit.secondaryL10nId = "fxviewtabrow-options-menu-button"; + visit.secondaryL10nArgs = JSON.stringify({ + tabTitle: visit.title || visit.url, + }); + } + + async #getVisitsForSearchQuery(searchQuery) { + let items = []; + try { + items = await this.placesQuery.searchHistory( + searchQuery, + this.searchResultsLimit + ); + } catch (e) { + getLogger("HistoryController").warn( + "There is a new search query in progress, so cancelling this one.", + e + ); + } + return [{ items }]; + } + + async #getVisitsForSortOption(sortOption, historyMap) { + if (!historyMap) { + historyMap = await this.#fetchHistory(); + } + switch (sortOption) { + case "date": + this.#setTodaysDate(); + return this.#getVisitsForDate(historyMap); + case "site": + return this.#getVisitsForSite(historyMap); + default: + return []; + } + } + + #setTodaysDate() { + const now = new Date(); + this.#todaysDate = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + ); + this.#yesterdaysDate = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() - 1 + ); + } + + /** + * Get a list of visits, sorted by date, in reverse chronological order. + * + * @param {Map<number, HistoryVisit[]>} historyMap + * @returns {CardEntry[]} + */ + #getVisitsForDate(historyMap) { + const entries = []; + const visitsFromToday = this.#getVisitsFromToday(historyMap); + const visitsFromYesterday = this.#getVisitsFromYesterday(historyMap); + const visitsByDay = this.#getVisitsByDay(historyMap); + const visitsByMonth = this.#getVisitsByMonth(historyMap); + + // Add visits from today and yesterday. + if (visitsFromToday.length) { + entries.push({ + l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-today"], + items: visitsFromToday, + }); + } + if (visitsFromYesterday.length) { + entries.push({ + l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-yesterday"], + items: visitsFromYesterday, + }); + } + + // Add visits from this month, grouped by day. + visitsByDay.forEach(visits => { + entries.push({ + l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-this-month"], + items: visits, + }); + }); + + // Add visits from previous months, grouped by month. + visitsByMonth.forEach(visits => { + entries.push({ + l10nId: HISTORY_MAP_L10N_IDS[this.component]["history-date-prev-month"], + items: visits, + }); + }); + return entries; + } + + #getVisitsFromToday(cachedHistory) { + const mapKey = this.placesQuery.getStartOfDayTimestamp(this.#todaysDate); + const visits = cachedHistory.get(mapKey) ?? []; + return [...visits]; + } + + #getVisitsFromYesterday(cachedHistory) { + const mapKey = this.placesQuery.getStartOfDayTimestamp( + this.#yesterdaysDate + ); + const visits = cachedHistory.get(mapKey) ?? []; + return [...visits]; + } + + /** + * Get a list of visits per day for each day on this month, excluding today + * and yesterday. + * + * @param {Map<number, HistoryVisit[]>} cachedHistory + * The history cache to process. + * @returns {HistoryVisit[][]} + * A list of visits for each day. + */ + #getVisitsByDay(cachedHistory) { + const visitsPerDay = []; + for (const [time, visits] of cachedHistory.entries()) { + const date = new Date(time); + if ( + this.#isSameDate(date, this.#todaysDate) || + this.#isSameDate(date, this.#yesterdaysDate) + ) { + continue; + } else if (!this.#isSameMonth(date, this.#todaysDate)) { + break; + } else { + visitsPerDay.push(visits); + } + } + return visitsPerDay; + } + + /** + * Get a list of visits per month for each month, excluding this one, and + * excluding yesterday's visits if yesterday happens to fall on the previous + * month. + * + * @param {Map<number, HistoryVisit[]>} cachedHistory + * The history cache to process. + * @returns {HistoryVisit[][]} + * A list of visits for each month. + */ + #getVisitsByMonth(cachedHistory) { + const visitsPerMonth = []; + let previousMonth = null; + for (const [time, visits] of cachedHistory.entries()) { + const date = new Date(time); + if ( + this.#isSameMonth(date, this.#todaysDate) || + this.#isSameDate(date, this.#yesterdaysDate) + ) { + continue; + } + const month = this.placesQuery.getStartOfMonthTimestamp(date); + if (month !== previousMonth) { + visitsPerMonth.push(visits); + } else { + visitsPerMonth[visitsPerMonth.length - 1] = visitsPerMonth + .at(-1) + .concat(visits); + } + previousMonth = month; + } + return visitsPerMonth; + } + + /** + * Given two date instances, check if their dates are equivalent. + * + * @param {Date} dateToCheck + * @param {Date} date + * @returns {boolean} + * Whether both date instances have equivalent dates. + */ + #isSameDate(dateToCheck, date) { + return ( + dateToCheck.getDate() === date.getDate() && + this.#isSameMonth(dateToCheck, date) + ); + } + + /** + * Given two date instances, check if their months are equivalent. + * + * @param {Date} dateToCheck + * @param {Date} month + * @returns {boolean} + * Whether both date instances have equivalent months. + */ + #isSameMonth(dateToCheck, month) { + return ( + dateToCheck.getMonth() === month.getMonth() && + dateToCheck.getFullYear() === month.getFullYear() + ); + } + + /** + * Get a list of visits, sorted by site, in alphabetical order. + * + * @param {Map<string, HistoryVisit[]>} historyMap + * @returns {CardEntry[]} + */ + #getVisitsForSite(historyMap) { + return Array.from(historyMap.entries(), ([domain, items]) => ({ + domain, + items, + l10nId: domain ? null : "firefoxview-history-site-localhost", + })).sort((a, b) => a.domain.localeCompare(b.domain)); + } + + async #fetchHistory() { + return this.placesQuery.getHistory({ + daysOld: 60, + limit: lazy.maxRowsPref, + sortBy: this.sortOption, + }); + } +} |