forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			383 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			383 lines
		
	
	
	
		
			10 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/. */
 | |
| 
 | |
| 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,
 | |
|     });
 | |
|   }
 | |
| }
 | 
