forked from mirrors/gecko-dev
		
	This better reflects the file is being accessed from both content and system scopes. Differential Revision: https://phabricator.services.mozilla.com/D203400
		
			
				
	
	
		
			322 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			322 lines
		
	
	
	
		
			11 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 { actionTypes as at } from "resource://activity-stream/common/Actions.mjs";
 | 
						|
 | 
						|
import { shortURL } from "resource://activity-stream/lib/ShortURL.sys.mjs";
 | 
						|
import {
 | 
						|
  TOP_SITES_DEFAULT_ROWS,
 | 
						|
  TOP_SITES_MAX_SITES_PER_ROW,
 | 
						|
} from "resource://activity-stream/common/Reducers.sys.mjs";
 | 
						|
import { Dedupe } from "resource://activity-stream/common/Dedupe.sys.mjs";
 | 
						|
 | 
						|
const lazy = {};
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						|
  DownloadsManager: "resource://activity-stream/lib/DownloadsManager.sys.mjs",
 | 
						|
  FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs",
 | 
						|
  LinksCache: "resource://activity-stream/lib/LinksCache.sys.mjs",
 | 
						|
  NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
 | 
						|
  PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
 | 
						|
  Screenshots: "resource://activity-stream/lib/Screenshots.sys.mjs",
 | 
						|
  SectionsManager: "resource://activity-stream/lib/SectionsManager.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
const HIGHLIGHTS_MAX_LENGTH = 16;
 | 
						|
 | 
						|
export const MANY_EXTRA_LENGTH =
 | 
						|
  HIGHLIGHTS_MAX_LENGTH * 5 +
 | 
						|
  TOP_SITES_DEFAULT_ROWS * TOP_SITES_MAX_SITES_PER_ROW;
 | 
						|
 | 
						|
export const SECTION_ID = "highlights";
 | 
						|
export const SYNC_BOOKMARKS_FINISHED_EVENT = "weave:engine:sync:applied";
 | 
						|
export const BOOKMARKS_RESTORE_SUCCESS_EVENT = "bookmarks-restore-success";
 | 
						|
export const BOOKMARKS_RESTORE_FAILED_EVENT = "bookmarks-restore-failed";
 | 
						|
const RECENT_DOWNLOAD_THRESHOLD = 36 * 60 * 60 * 1000;
 | 
						|
 | 
						|
export class HighlightsFeed {
 | 
						|
  constructor() {
 | 
						|
    this.dedupe = new Dedupe(this._dedupeKey);
 | 
						|
    this.linksCache = new lazy.LinksCache(
 | 
						|
      lazy.NewTabUtils.activityStreamLinks,
 | 
						|
      "getHighlights",
 | 
						|
      ["image"]
 | 
						|
    );
 | 
						|
    lazy.PageThumbs.addExpirationFilter(this);
 | 
						|
    this.downloadsManager = new lazy.DownloadsManager();
 | 
						|
  }
 | 
						|
 | 
						|
  _dedupeKey(site) {
 | 
						|
    // Treat bookmarks, pocket, and downloaded items as un-dedupable, otherwise show one of a url
 | 
						|
    return (
 | 
						|
      site &&
 | 
						|
      (site.pocket_id || site.type === "bookmark" || site.type === "download"
 | 
						|
        ? {}
 | 
						|
        : site.url)
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  init() {
 | 
						|
    Services.obs.addObserver(this, SYNC_BOOKMARKS_FINISHED_EVENT);
 | 
						|
    Services.obs.addObserver(this, BOOKMARKS_RESTORE_SUCCESS_EVENT);
 | 
						|
    Services.obs.addObserver(this, BOOKMARKS_RESTORE_FAILED_EVENT);
 | 
						|
    lazy.SectionsManager.onceInitialized(this.postInit.bind(this));
 | 
						|
  }
 | 
						|
 | 
						|
  postInit() {
 | 
						|
    lazy.SectionsManager.enableSection(SECTION_ID, true /* isStartup */);
 | 
						|
    this.fetchHighlights({ broadcast: true, isStartup: true });
 | 
						|
    this.downloadsManager.init(this.store);
 | 
						|
  }
 | 
						|
 | 
						|
  uninit() {
 | 
						|
    lazy.SectionsManager.disableSection(SECTION_ID);
 | 
						|
    lazy.PageThumbs.removeExpirationFilter(this);
 | 
						|
    Services.obs.removeObserver(this, SYNC_BOOKMARKS_FINISHED_EVENT);
 | 
						|
    Services.obs.removeObserver(this, BOOKMARKS_RESTORE_SUCCESS_EVENT);
 | 
						|
    Services.obs.removeObserver(this, BOOKMARKS_RESTORE_FAILED_EVENT);
 | 
						|
  }
 | 
						|
 | 
						|
  observe(subject, topic, data) {
 | 
						|
    // When we receive a notification that a sync has happened for bookmarks,
 | 
						|
    // or Places finished importing or restoring bookmarks, refresh highlights
 | 
						|
    const manyBookmarksChanged =
 | 
						|
      (topic === SYNC_BOOKMARKS_FINISHED_EVENT && data === "bookmarks") ||
 | 
						|
      topic === BOOKMARKS_RESTORE_SUCCESS_EVENT ||
 | 
						|
      topic === BOOKMARKS_RESTORE_FAILED_EVENT;
 | 
						|
    if (manyBookmarksChanged) {
 | 
						|
      this.fetchHighlights({ broadcast: true });
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  filterForThumbnailExpiration(callback) {
 | 
						|
    const state = this.store
 | 
						|
      .getState()
 | 
						|
      .Sections.find(section => section.id === SECTION_ID);
 | 
						|
 | 
						|
    callback(
 | 
						|
      state && state.initialized
 | 
						|
        ? state.rows.reduce((acc, site) => {
 | 
						|
            // Screenshots call in `fetchImage` will search for preview_image_url or
 | 
						|
            // fallback to URL, so we prevent both from being expired.
 | 
						|
            acc.push(site.url);
 | 
						|
            if (site.preview_image_url) {
 | 
						|
              acc.push(site.preview_image_url);
 | 
						|
            }
 | 
						|
            return acc;
 | 
						|
          }, [])
 | 
						|
        : []
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Chronologically sort highlights of all types except 'visited'. Then just append
 | 
						|
   * the rest at the end of highlights.
 | 
						|
   * @param {Array} pages The full list of links to order.
 | 
						|
   * @return {Array} A sorted array of highlights
 | 
						|
   */
 | 
						|
  _orderHighlights(pages) {
 | 
						|
    const splitHighlights = { chronologicalCandidates: [], visited: [] };
 | 
						|
    for (let page of pages) {
 | 
						|
      if (page.type === "history") {
 | 
						|
        splitHighlights.visited.push(page);
 | 
						|
      } else {
 | 
						|
        splitHighlights.chronologicalCandidates.push(page);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return splitHighlights.chronologicalCandidates
 | 
						|
      .sort((a, b) => a.date_added < b.date_added)
 | 
						|
      .concat(splitHighlights.visited);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Refresh the highlights data for content.
 | 
						|
   * @param {bool} options.broadcast Should the update be broadcasted.
 | 
						|
   */
 | 
						|
  async fetchHighlights(options = {}) {
 | 
						|
    // If TopSites are enabled we need them for deduping, so wait for
 | 
						|
    // TOP_SITES_UPDATED. We also need the section to be registered to update
 | 
						|
    // state, so wait for postInit triggered by lazy.SectionsManager initializing.
 | 
						|
    if (
 | 
						|
      (!this.store.getState().TopSites.initialized &&
 | 
						|
        this.store.getState().Prefs.values["feeds.system.topsites"] &&
 | 
						|
        this.store.getState().Prefs.values["feeds.topsites"]) ||
 | 
						|
      !this.store.getState().Sections.length
 | 
						|
    ) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // We broadcast when we want to force an update, so get fresh links
 | 
						|
    if (options.broadcast) {
 | 
						|
      this.linksCache.expire();
 | 
						|
    }
 | 
						|
 | 
						|
    // Request more than the expected length to allow for items being removed by
 | 
						|
    // deduping against Top Sites or multiple history from the same domain, etc.
 | 
						|
    const manyPages = await this.linksCache.request({
 | 
						|
      numItems: MANY_EXTRA_LENGTH,
 | 
						|
      excludeBookmarks:
 | 
						|
        !this.store.getState().Prefs.values[
 | 
						|
          "section.highlights.includeBookmarks"
 | 
						|
        ],
 | 
						|
      excludeHistory:
 | 
						|
        !this.store.getState().Prefs.values[
 | 
						|
          "section.highlights.includeVisited"
 | 
						|
        ],
 | 
						|
      excludePocket:
 | 
						|
        !this.store.getState().Prefs.values["section.highlights.includePocket"],
 | 
						|
    });
 | 
						|
 | 
						|
    if (
 | 
						|
      this.store.getState().Prefs.values["section.highlights.includeDownloads"]
 | 
						|
    ) {
 | 
						|
      // We only want 1 download that is less than 36 hours old, and the file currently exists
 | 
						|
      let results = await this.downloadsManager.getDownloads(
 | 
						|
        RECENT_DOWNLOAD_THRESHOLD,
 | 
						|
        { numItems: 1, onlySucceeded: true, onlyExists: true }
 | 
						|
      );
 | 
						|
      if (results.length) {
 | 
						|
        // We only want 1 download, the most recent one
 | 
						|
        manyPages.push({
 | 
						|
          ...results[0],
 | 
						|
          type: "download",
 | 
						|
        });
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const orderedPages = this._orderHighlights(manyPages);
 | 
						|
 | 
						|
    // Remove adult highlights if we need to
 | 
						|
    const checkedAdult = lazy.FilterAdult.filter(orderedPages);
 | 
						|
 | 
						|
    // Remove any Highlights that are in Top Sites already
 | 
						|
    const [, deduped] = this.dedupe.group(
 | 
						|
      this.store.getState().TopSites.rows,
 | 
						|
      checkedAdult
 | 
						|
    );
 | 
						|
 | 
						|
    // Keep all "bookmark"s and at most one (most recent) "history" per host
 | 
						|
    const highlights = [];
 | 
						|
    const hosts = new Set();
 | 
						|
    for (const page of deduped) {
 | 
						|
      const hostname = shortURL(page);
 | 
						|
      // Skip this history page if we already something from the same host
 | 
						|
      if (page.type === "history" && hosts.has(hostname)) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      // If we already have the image for the card, use that immediately. Else
 | 
						|
      // asynchronously fetch the image. NEVER fetch a screenshot for downloads
 | 
						|
      if (!page.image && page.type !== "download") {
 | 
						|
        this.fetchImage(page, options.isStartup);
 | 
						|
      }
 | 
						|
 | 
						|
      // Adjust the type for 'history' items that are also 'bookmarked' when we
 | 
						|
      // want to include bookmarks
 | 
						|
      if (
 | 
						|
        page.type === "history" &&
 | 
						|
        page.bookmarkGuid &&
 | 
						|
        this.store.getState().Prefs.values[
 | 
						|
          "section.highlights.includeBookmarks"
 | 
						|
        ]
 | 
						|
      ) {
 | 
						|
        page.type = "bookmark";
 | 
						|
      }
 | 
						|
 | 
						|
      // We want the page, so update various fields for UI
 | 
						|
      Object.assign(page, {
 | 
						|
        hasImage: page.type !== "download", // Downloads do not have an image - all else types fall back to a screenshot
 | 
						|
        hostname,
 | 
						|
        type: page.type,
 | 
						|
        pocket_id: page.pocket_id,
 | 
						|
      });
 | 
						|
 | 
						|
      // Add the "bookmark", "pocket", or not-skipped "history"
 | 
						|
      highlights.push(page);
 | 
						|
      hosts.add(hostname);
 | 
						|
 | 
						|
      // Remove internal properties that might be updated after dispatch
 | 
						|
      delete page.__sharedCache;
 | 
						|
 | 
						|
      // Skip the rest if we have enough items
 | 
						|
      if (highlights.length === HIGHLIGHTS_MAX_LENGTH) {
 | 
						|
        break;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const { initialized } = this.store
 | 
						|
      .getState()
 | 
						|
      .Sections.find(section => section.id === SECTION_ID);
 | 
						|
    // Broadcast when required or if it is the first update.
 | 
						|
    const shouldBroadcast = options.broadcast || !initialized;
 | 
						|
 | 
						|
    lazy.SectionsManager.updateSection(
 | 
						|
      SECTION_ID,
 | 
						|
      { rows: highlights },
 | 
						|
      shouldBroadcast,
 | 
						|
      options.isStartup
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Fetch an image for a given highlight and update the card with it. If no
 | 
						|
   * image is available then fallback to fetching a screenshot.
 | 
						|
   */
 | 
						|
  fetchImage(page, isStartup = false) {
 | 
						|
    // Request a screenshot if we don't already have one pending
 | 
						|
    const { preview_image_url: imageUrl, url } = page;
 | 
						|
    return lazy.Screenshots.maybeCacheScreenshot(
 | 
						|
      page,
 | 
						|
      imageUrl || url,
 | 
						|
      "image",
 | 
						|
      image => {
 | 
						|
        lazy.SectionsManager.updateSectionCard(
 | 
						|
          SECTION_ID,
 | 
						|
          url,
 | 
						|
          { image },
 | 
						|
          true,
 | 
						|
          isStartup
 | 
						|
        );
 | 
						|
      }
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  onAction(action) {
 | 
						|
    // Relay the downloads actions to DownloadsManager - it is a child of HighlightsFeed
 | 
						|
    this.downloadsManager.onAction(action);
 | 
						|
    switch (action.type) {
 | 
						|
      case at.INIT:
 | 
						|
        this.init();
 | 
						|
        break;
 | 
						|
      case at.SYSTEM_TICK:
 | 
						|
      case at.TOP_SITES_UPDATED:
 | 
						|
        this.fetchHighlights({
 | 
						|
          broadcast: false,
 | 
						|
          isStartup: !!action.meta?.isStartup,
 | 
						|
        });
 | 
						|
        break;
 | 
						|
      case at.PREF_CHANGED:
 | 
						|
        // Update existing pages when the user changes what should be shown
 | 
						|
        if (action.data.name.startsWith("section.highlights.include")) {
 | 
						|
          this.fetchHighlights({ broadcast: true });
 | 
						|
        }
 | 
						|
        break;
 | 
						|
      case at.PLACES_HISTORY_CLEARED:
 | 
						|
      case at.PLACES_LINK_BLOCKED:
 | 
						|
      case at.DOWNLOAD_CHANGED:
 | 
						|
      case at.POCKET_LINK_DELETED_OR_ARCHIVED:
 | 
						|
        this.fetchHighlights({ broadcast: true });
 | 
						|
        break;
 | 
						|
      case at.PLACES_LINKS_CHANGED:
 | 
						|
      case at.PLACES_SAVED_TO_POCKET:
 | 
						|
        this.linksCache.expire();
 | 
						|
        this.fetchHighlights({ broadcast: false });
 | 
						|
        break;
 | 
						|
      case at.UNINIT:
 | 
						|
        this.uninit();
 | 
						|
        break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |