mirror of
				https://github.com/mozilla/gecko-dev.git
				synced 2025-11-04 10:18:41 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1068 lines
		
	
	
	
		
			32 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1068 lines
		
	
	
	
		
			32 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
 | 
						||
 | 
						||
const lazy = {};
 | 
						||
ChromeUtils.defineESModuleGetters(lazy, {
 | 
						||
  LinkPreviewModel:
 | 
						||
    "moz-src:///browser/components/genai/LinkPreviewModel.sys.mjs",
 | 
						||
  NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
 | 
						||
  PrefUtils: "resource://normandy/lib/PrefUtils.sys.mjs",
 | 
						||
  Region: "resource://gre/modules/Region.sys.mjs",
 | 
						||
});
 | 
						||
 | 
						||
export const LABS_STATE = Object.freeze({
 | 
						||
  NOT_ENROLLED: 0,
 | 
						||
  ENROLLED: 1,
 | 
						||
  ROLLOUT_ENDED: 2,
 | 
						||
});
 | 
						||
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "allowedLanguages",
 | 
						||
  "browser.ml.linkPreview.allowedLanguages"
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "collapsed",
 | 
						||
  "browser.ml.linkPreview.collapsed",
 | 
						||
  null,
 | 
						||
  (_pref, _old, val) => LinkPreview.onCollapsedPref(val)
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "enabled",
 | 
						||
  "browser.ml.linkPreview.enabled",
 | 
						||
  null,
 | 
						||
  (_pref, _old, val) => LinkPreview.onEnabledPref(val)
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "ignoreMs",
 | 
						||
  "browser.ml.linkPreview.ignoreMs"
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "labs",
 | 
						||
  "browser.ml.linkPreview.labs",
 | 
						||
  LABS_STATE.NOT_ENROLLED
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "longPress",
 | 
						||
  "browser.ml.linkPreview.longPress",
 | 
						||
  null,
 | 
						||
  (_pref, _old, val) => LinkPreview.onLongPressPrefChange(val)
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "longPressMs",
 | 
						||
  "browser.ml.linkPreview.longPressMs"
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "nimbus",
 | 
						||
  "browser.ml.linkPreview.nimbus"
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "noKeyPointsRegions",
 | 
						||
  "browser.ml.linkPreview.noKeyPointsRegions"
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "onboardingCooldownPeriodMs",
 | 
						||
  "browser.ml.linkPreview.onboardingCooldownPeriodMs",
 | 
						||
  7 * 24 * 60 * 60 * 1000 // Constant for onboarding reactivation cooldown period (7 days in milliseconds)
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "onboardingHoverLinkMs",
 | 
						||
  "browser.ml.linkPreview.onboardingHoverLinkMs",
 | 
						||
  1000
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "onboardingMaxShowFreq",
 | 
						||
  "browser.ml.linkPreview.onboardingMaxShowFreq",
 | 
						||
  2
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "onboardingTimes",
 | 
						||
  "browser.ml.linkPreview.onboardingTimes",
 | 
						||
  "", // default (when PREF_INVALID)
 | 
						||
  null, // no onUpdate callback
 | 
						||
  rawValue => {
 | 
						||
    if (!rawValue) {
 | 
						||
      return [];
 | 
						||
    }
 | 
						||
    return rawValue.split(",").map(Number);
 | 
						||
  }
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "optin",
 | 
						||
  "browser.ml.linkPreview.optin",
 | 
						||
  null,
 | 
						||
  (_pref, _old, val) => LinkPreview.onOptinPref(val)
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "prefetchOnEnable",
 | 
						||
  "browser.ml.linkPreview.prefetchOnEnable",
 | 
						||
  true
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "recentTypingMs",
 | 
						||
  "browser.ml.linkPreview.recentTypingMs"
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "shift",
 | 
						||
  "browser.ml.linkPreview.shift",
 | 
						||
  null,
 | 
						||
  (_pref, _old, val) => LinkPreview.onShiftPrefChange(val)
 | 
						||
);
 | 
						||
XPCOMUtils.defineLazyPreferenceGetter(
 | 
						||
  lazy,
 | 
						||
  "shiftAlt",
 | 
						||
  "browser.ml.linkPreview.shiftAlt",
 | 
						||
  null,
 | 
						||
  (_pref, _old, val) => LinkPreview.onShiftAltPrefChange(val)
 | 
						||
);
 | 
						||
 | 
						||
export const LinkPreview = {
 | 
						||
  // Shared downloading state to use across multiple previews
 | 
						||
  progress: -1, // -1 = off, 0-100 = download progress
 | 
						||
 | 
						||
  cancelLongPress: null,
 | 
						||
  keyboardComboActive: false,
 | 
						||
  overLinkTime: 0,
 | 
						||
  recentTyping: 0,
 | 
						||
  _windowStates: new Map(),
 | 
						||
  linkPreviewPanelId: "link-preview-panel",
 | 
						||
 | 
						||
  /**
 | 
						||
   * Gets the context value for the current tab.
 | 
						||
   * For about: pages, returns the URI's filePath (e.g., "home", "newtab", "preferences").
 | 
						||
   * For regular webpages, returns undefined.
 | 
						||
   *
 | 
						||
   * @param {Window} win - The browser window context.
 | 
						||
   * @returns {string|undefined} The tab context value or undefined if not an about: page.
 | 
						||
   * @private
 | 
						||
   */
 | 
						||
  _getTabContextValue(win) {
 | 
						||
    const uri = win.gBrowser.selectedBrowser.currentURI;
 | 
						||
    // Check if uri exists, scheme is 'about', and filePath is a truthy string
 | 
						||
    if (uri?.scheme === "about" && uri.filePath) {
 | 
						||
      return uri.filePath;
 | 
						||
    }
 | 
						||
    return undefined;
 | 
						||
  },
 | 
						||
 | 
						||
  get canShowKeyPoints() {
 | 
						||
    return this._isRegionSupported();
 | 
						||
  },
 | 
						||
 | 
						||
  get canShowLegacy() {
 | 
						||
    return lazy.labs != LABS_STATE.NOT_ENROLLED;
 | 
						||
  },
 | 
						||
 | 
						||
  get canShowPreferences() {
 | 
						||
    return lazy.enabled;
 | 
						||
  },
 | 
						||
 | 
						||
  get showOnboarding() {
 | 
						||
    const timesArray = lazy.onboardingTimes;
 | 
						||
 | 
						||
    const lastValidTime = timesArray.at(-1) || 0;
 | 
						||
    const timeSinceLastOnboarding = Date.now() - lastValidTime;
 | 
						||
 | 
						||
    return (
 | 
						||
      timesArray.length < lazy.onboardingMaxShowFreq &&
 | 
						||
      timeSinceLastOnboarding >= lazy.onboardingCooldownPeriodMs
 | 
						||
    );
 | 
						||
  },
 | 
						||
 | 
						||
  shouldShowContextMenu(nsContextMenu) {
 | 
						||
    // In a future patch, we can further analyze the link, etc.
 | 
						||
    //link url value: nsContextMenu.linkURL
 | 
						||
    // For now, let’s rely on whether LinkPreview is enabled and region supported
 | 
						||
    //link conditions are borrowed from context-stripOnShareLink
 | 
						||
 | 
						||
    return (
 | 
						||
      this._isRegionSupported() &&
 | 
						||
      lazy.enabled &&
 | 
						||
      (nsContextMenu.onLink || nsContextMenu.onPlainTextLink) &&
 | 
						||
      !nsContextMenu.onMailtoLink &&
 | 
						||
      !nsContextMenu.onTelLink &&
 | 
						||
      !nsContextMenu.onMozExtLink
 | 
						||
    );
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles the preference change for the 'shift' key activation.
 | 
						||
   *
 | 
						||
   * @param {boolean} enabled - The new state of the shift key preference.
 | 
						||
   */
 | 
						||
  onShiftPrefChange(enabled) {
 | 
						||
    Glean.genaiLinkpreview.prefChanged.record({ enabled, pref: "shift" });
 | 
						||
    this._updateShortcutMetric();
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles the preference change for the 'shift+alt' key activation.
 | 
						||
   *
 | 
						||
   * @param {boolean} enabled - The new state of the shift+alt key preference.
 | 
						||
   */
 | 
						||
  onShiftAltPrefChange(enabled) {
 | 
						||
    Glean.genaiLinkpreview.prefChanged.record({
 | 
						||
      enabled,
 | 
						||
      pref: "shift_alt",
 | 
						||
    });
 | 
						||
    this._updateShortcutMetric();
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles the preference change for the long press activation.
 | 
						||
   *
 | 
						||
   * @param {boolean} enabled - The new state of the long press preference.
 | 
						||
   */
 | 
						||
  onLongPressPrefChange(enabled) {
 | 
						||
    Glean.genaiLinkpreview.prefChanged.record({
 | 
						||
      enabled,
 | 
						||
      pref: "long_press",
 | 
						||
    });
 | 
						||
    this._updateShortcutMetric();
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles the preference change for enabling/disabling Link Preview.
 | 
						||
   * It adds or removes event listeners for all tracked windows based on the new preference value.
 | 
						||
   *
 | 
						||
   * @param {boolean} enabled - The new state of the Link Preview preference.
 | 
						||
   */
 | 
						||
  onEnabledPref(enabled) {
 | 
						||
    const method = enabled ? "_addEventListeners" : "_removeEventListeners";
 | 
						||
    for (const win of this._windowStates.keys()) {
 | 
						||
      this[method](win);
 | 
						||
    }
 | 
						||
 | 
						||
    // Prefetch the model when enabling by simulating a request.
 | 
						||
    if (enabled && lazy.prefetchOnEnable && this._isRegionSupported()) {
 | 
						||
      this.generateKeyPoints();
 | 
						||
    }
 | 
						||
 | 
						||
    Glean.genaiLinkpreview.enabled.set(enabled);
 | 
						||
    Glean.genaiLinkpreview.prefChanged.record({
 | 
						||
      enabled,
 | 
						||
      pref: "link_previews",
 | 
						||
    });
 | 
						||
 | 
						||
    this.handleNimbusPrefs();
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Updates a property on the link-preview-card element for all window states.
 | 
						||
   *
 | 
						||
   * @param {string} prop - The property to update.
 | 
						||
   * @param {*} value - The value to set for the property.
 | 
						||
   */
 | 
						||
  updateCardProperty(prop, value) {
 | 
						||
    for (const [win] of this._windowStates) {
 | 
						||
      const panel = win.document.getElementById(this.linkPreviewPanelId);
 | 
						||
      if (!panel) {
 | 
						||
        continue;
 | 
						||
      }
 | 
						||
 | 
						||
      const card = panel.querySelector("link-preview-card");
 | 
						||
      if (card) {
 | 
						||
        card[prop] = value;
 | 
						||
      }
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles the preference change for opt-in state.
 | 
						||
   * Updates all link preview cards with the new opt-in state.
 | 
						||
   *
 | 
						||
   * @param {boolean} optin - The new state of the opt-in preference.
 | 
						||
   */
 | 
						||
  onOptinPref(optin) {
 | 
						||
    this.updateCardProperty("optin", optin);
 | 
						||
    Glean.genaiLinkpreview.cardAiConsent.record({
 | 
						||
      option: optin ? "continue" : "cancel",
 | 
						||
    });
 | 
						||
    Glean.genaiLinkpreview.prefChanged.record({
 | 
						||
      enabled: optin,
 | 
						||
      pref: "key_points",
 | 
						||
    });
 | 
						||
    Glean.genaiLinkpreview.aiOptin.set(optin);
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles the preference change for collapsed state.
 | 
						||
   * Updates all link preview cards with the new collapsed state.
 | 
						||
   *
 | 
						||
   * @param {boolean} collapsed - The new state of the collapsed preference.
 | 
						||
   */
 | 
						||
  onCollapsedPref(collapsed) {
 | 
						||
    this.updateCardProperty("collapsed", collapsed);
 | 
						||
    Glean.genaiLinkpreview.keyPointsToggle.record({
 | 
						||
      expand: !collapsed,
 | 
						||
    });
 | 
						||
    Glean.genaiLinkpreview.keyPoints.set(!collapsed);
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles Nimbus preferences, e.g., migrating, restoring, setting.
 | 
						||
   */
 | 
						||
  handleNimbusPrefs() {
 | 
						||
    // For those who turned on via labs with enabled setPref variable, persist
 | 
						||
    // the pref and allow using shift_alt matching labs copy.
 | 
						||
    if (
 | 
						||
      lazy.NimbusFeatures.linkPreviews.getVariable("enabled") &&
 | 
						||
      lazy.labs == LABS_STATE.NOT_ENROLLED
 | 
						||
    ) {
 | 
						||
      Services.prefs.setIntPref(
 | 
						||
        "browser.ml.linkPreview.labs",
 | 
						||
        LABS_STATE.ENROLLED
 | 
						||
      );
 | 
						||
      Services.prefs.setBoolPref("browser.ml.linkPreview.shiftAlt", true);
 | 
						||
    }
 | 
						||
    // Restore pref once if previously enabled via labs assuming rollout ended.
 | 
						||
    else if (!lazy.enabled && lazy.labs == LABS_STATE.ENROLLED) {
 | 
						||
      Services.prefs.setIntPref(
 | 
						||
        "browser.ml.linkPreview.labs",
 | 
						||
        LABS_STATE.ROLLOUT_ENDED
 | 
						||
      );
 | 
						||
      Services.prefs.setBoolPref("browser.ml.linkPreview.enabled", true);
 | 
						||
    }
 | 
						||
 | 
						||
    // Handle nimbus feature pref setting
 | 
						||
    if (this._nimbusRegistered) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    this._nimbusRegistered = true;
 | 
						||
    const featureId = "linkPreviews";
 | 
						||
    lazy.NimbusFeatures[featureId].onUpdate(() => {
 | 
						||
      const enrollment = lazy.NimbusFeatures[featureId].getEnrollmentMetadata();
 | 
						||
      if (!enrollment) {
 | 
						||
        return;
 | 
						||
      }
 | 
						||
 | 
						||
      // Set prefs on any branch if we have a new enrollment slug, otherwise
 | 
						||
      // only set default branch as those only last for the session
 | 
						||
      const slug = enrollment.slug + ":" + enrollment.branch;
 | 
						||
      const anyBranch = slug != lazy.nimbus;
 | 
						||
      const setPref = ([pref, { branch = "user", value = null }]) => {
 | 
						||
        if (anyBranch || branch == "default") {
 | 
						||
          lazy.PrefUtils.setPref("browser.ml.linkPreview." + pref, value, {
 | 
						||
            branch,
 | 
						||
          });
 | 
						||
        }
 | 
						||
      };
 | 
						||
      setPref(["nimbus", { value: slug }]);
 | 
						||
      Object.entries(
 | 
						||
        lazy.NimbusFeatures[featureId].getVariable("prefs") ?? []
 | 
						||
      ).forEach(setPref);
 | 
						||
    });
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles startup tasks such as telemetry and adding listeners.
 | 
						||
   *
 | 
						||
   * @param {Window} win - The window context used to add event listeners.
 | 
						||
   */
 | 
						||
  init(win) {
 | 
						||
    // Access getters for side effects of observing pref changes
 | 
						||
    lazy.collapsed;
 | 
						||
    lazy.enabled;
 | 
						||
    lazy.longPress;
 | 
						||
    lazy.optin;
 | 
						||
    lazy.shift;
 | 
						||
    lazy.shiftAlt;
 | 
						||
 | 
						||
    this._windowStates.set(win, {});
 | 
						||
    if (!win.customElements.get("link-preview-card")) {
 | 
						||
      win.ChromeUtils.importESModule(
 | 
						||
        "chrome://browser/content/genai/content/link-preview-card.mjs",
 | 
						||
        { global: "current" }
 | 
						||
      );
 | 
						||
    }
 | 
						||
    if (!win.customElements.get("link-preview-card-onboarding")) {
 | 
						||
      win.ChromeUtils.importESModule(
 | 
						||
        "chrome://browser/content/genai/content/link-preview-card-onboarding.mjs",
 | 
						||
        { global: "current" }
 | 
						||
      );
 | 
						||
    }
 | 
						||
 | 
						||
    this.handleNimbusPrefs();
 | 
						||
 | 
						||
    if (lazy.enabled) {
 | 
						||
      this._addEventListeners(win);
 | 
						||
    }
 | 
						||
 | 
						||
    Glean.genaiLinkpreview.aiOptin.set(lazy.optin);
 | 
						||
    Glean.genaiLinkpreview.enabled.set(lazy.enabled);
 | 
						||
    Glean.genaiLinkpreview.keyPoints.set(!lazy.collapsed);
 | 
						||
    this._updateShortcutMetric();
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Teardown the Link Preview feature for the given window.
 | 
						||
   * Removes event listeners from the specified window and removes it from the window map.
 | 
						||
   *
 | 
						||
   * @param {Window} win - The window context to uninitialize.
 | 
						||
   */
 | 
						||
  teardown(win) {
 | 
						||
    // Remove event listeners from the specified window
 | 
						||
    if (lazy.enabled) {
 | 
						||
      this._removeEventListeners(win);
 | 
						||
    }
 | 
						||
 | 
						||
    // Remove the panel if it exists
 | 
						||
    const doc = win.document;
 | 
						||
    doc.getElementById(this.linkPreviewPanelId)?.remove();
 | 
						||
 | 
						||
    // Remove the window from the map
 | 
						||
    this._windowStates.delete(win);
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Adds all needed event listeners and updates the state.
 | 
						||
   *
 | 
						||
   * @param {Window} win - The window to which event listeners are added.
 | 
						||
   */
 | 
						||
  _addEventListeners(win) {
 | 
						||
    win.addEventListener("OverLink", this, true);
 | 
						||
    win.addEventListener("keydown", this, true);
 | 
						||
    win.addEventListener("keyup", this, true);
 | 
						||
    win.addEventListener("mousedown", this, true);
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Removes all event listeners and updates the state.
 | 
						||
   *
 | 
						||
   * @param {Window} win - The window from which event listeners are removed.
 | 
						||
   */
 | 
						||
  _removeEventListeners(win) {
 | 
						||
    win.removeEventListener("OverLink", this, true);
 | 
						||
    win.removeEventListener("keydown", this, true);
 | 
						||
    win.removeEventListener("keyup", this, true);
 | 
						||
    win.removeEventListener("mousedown", this, true);
 | 
						||
 | 
						||
    // Long press might have added listeners to this window.
 | 
						||
    this.cancelLongPress?.();
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles keyboard events ("keydown" and "keyup") for the Link Preview feature.
 | 
						||
   * Adjusts the state of keyboardComboActive based on modifier keys.
 | 
						||
   *
 | 
						||
   * @param {KeyboardEvent} event - The keyboard event to be processed.
 | 
						||
   */
 | 
						||
  handleEvent(event) {
 | 
						||
    switch (event.type) {
 | 
						||
      case "keydown":
 | 
						||
      case "keyup":
 | 
						||
        this._onKeyEvent(event);
 | 
						||
        break;
 | 
						||
      case "OverLink":
 | 
						||
        this._onLinkPreview(event);
 | 
						||
        break;
 | 
						||
      case "dragstart":
 | 
						||
      case "mousedown":
 | 
						||
      case "mouseup":
 | 
						||
        this._onPressEvent(event);
 | 
						||
        break;
 | 
						||
      default:
 | 
						||
        break;
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles "keydown" and "keyup" events.
 | 
						||
   *
 | 
						||
   * @param {KeyboardEvent} event - The keyboard event to be processed.
 | 
						||
   */
 | 
						||
  _onKeyEvent(event) {
 | 
						||
    const win = event.currentTarget;
 | 
						||
 | 
						||
    // Track regular typing to suppress keyboard previews.
 | 
						||
    if (event.key.length == 1 || ["Enter", "Tab"].includes(event.key)) {
 | 
						||
      this.recentTyping = Date.now();
 | 
						||
    }
 | 
						||
 | 
						||
    // Keyboard combos requires shift and neither ctrl nor meta.
 | 
						||
    this.keyboardComboActive = false;
 | 
						||
    if (!event.shiftKey || event.ctrlKey || event.metaKey) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    // Handle shift without alt if preference is set.
 | 
						||
    if (!event.altKey && lazy.shift) {
 | 
						||
      this.keyboardComboActive = "shift";
 | 
						||
    }
 | 
						||
    // Handle shift with alt if preference is set.
 | 
						||
    else if (event.altKey && lazy.shiftAlt) {
 | 
						||
      this.keyboardComboActive = "shift_alt";
 | 
						||
    }
 | 
						||
    // New presses or releases can result in desired combo for previewing.
 | 
						||
    this._maybeLinkPreview(win);
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles "OverLink" events.
 | 
						||
   * Stores the hovered link URL in the per-window state object and processes the
 | 
						||
   * link preview if the keyboard combination is active.
 | 
						||
   *
 | 
						||
   * @param {CustomEvent} event - The event object containing details about the link preview.
 | 
						||
   */
 | 
						||
  _onLinkPreview(event) {
 | 
						||
    const win = event.currentTarget;
 | 
						||
    const url = event.detail.url;
 | 
						||
 | 
						||
    // Store the current overLink in the per-window state object filtering out
 | 
						||
    // links common for dynamic single page apps.
 | 
						||
    const stateObject = this._windowStates.get(win);
 | 
						||
    stateObject.overLink =
 | 
						||
      url.endsWith("#") || url.startsWith("javascript:") ? "" : url;
 | 
						||
    this.overLinkTime = Date.now();
 | 
						||
 | 
						||
    // If the keyboard combo is active, always check for link preview
 | 
						||
    // regardless of whether it's the same URL.
 | 
						||
    if (this.keyboardComboActive) {
 | 
						||
      this._maybeLinkPreview(win);
 | 
						||
    } else if (this.showOnboarding) {
 | 
						||
      this._maybeOnboard(win, url, stateObject);
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  _maybeOnboard(win, url, stateObject) {
 | 
						||
    if (!url) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    const panel = win.document.getElementById(this.linkPreviewPanelId);
 | 
						||
    const isPanelOpen = panel && panel.state !== "closed";
 | 
						||
 | 
						||
    // If panel is open or it's the same URL as last hover, don't start
 | 
						||
    // hover-based onboarding timer.
 | 
						||
    if (isPanelOpen || url === stateObject.lastHoveredUrl) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    // Clear any existing timer when moving to a new link
 | 
						||
    if (stateObject.hoverTimerId) {
 | 
						||
      win.clearTimeout(stateObject.hoverTimerId);
 | 
						||
      stateObject.hoverTimerId = null;
 | 
						||
    }
 | 
						||
 | 
						||
    // Update last hovered URL
 | 
						||
    stateObject.lastHoveredUrl = url;
 | 
						||
    stateObject.hoverTimerId = win.setTimeout(() => {
 | 
						||
      // Only show if we're still hovering the same URL
 | 
						||
      if (stateObject.overLink === url) {
 | 
						||
        this.renderOnboardingPanel(win, url);
 | 
						||
      }
 | 
						||
      stateObject.lastHoveredUrl = "";
 | 
						||
      stateObject.hoverTimerId = null;
 | 
						||
    }, lazy.onboardingHoverLinkMs);
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Renders the onboarding panel for link preview.
 | 
						||
   * Updates onboardingTimes and renders onboarding card
 | 
						||
   *
 | 
						||
   * @param {Window} win - The browser window context.
 | 
						||
   * @param {string} url - The URL of the link to be previewed.
 | 
						||
   */
 | 
						||
  async renderOnboardingPanel(win, url) {
 | 
						||
    // Short-circuit if onboarding is no longer eligible - prevents race condition
 | 
						||
    // where onboarding might start rendering after showOnboarding status has changed
 | 
						||
    if (!this.showOnboarding) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    // Append the current time to onboarding times.
 | 
						||
    Services.prefs.setStringPref("browser.ml.linkPreview.onboardingTimes", [
 | 
						||
      ...lazy.onboardingTimes,
 | 
						||
      Date.now(),
 | 
						||
    ]);
 | 
						||
 | 
						||
    const doc = win.document;
 | 
						||
    const onboardingCard = doc.createElement("link-preview-card-onboarding");
 | 
						||
    onboardingCard.style.width = "100%";
 | 
						||
    onboardingCard.onboardingType = lazy.longPress ? "longPress" : "shiftKey";
 | 
						||
 | 
						||
    // Telemetry for onboarding card view
 | 
						||
    Glean.genaiLinkpreview.onboardingCard.record({
 | 
						||
      action: "view",
 | 
						||
      type: onboardingCard.onboardingType,
 | 
						||
    });
 | 
						||
 | 
						||
    // Now show the preview as an "onboarding" source
 | 
						||
    const panel = this.initOrResetPreviewPanel(win, "onboarding");
 | 
						||
    panel.onboardingType = onboardingCard.onboardingType;
 | 
						||
 | 
						||
    onboardingCard.addEventListener(
 | 
						||
      "LinkPreviewCard:onboardingComplete",
 | 
						||
      () => {
 | 
						||
        Glean.genaiLinkpreview.onboardingCard.record({
 | 
						||
          action: "try_it_now",
 | 
						||
          type: onboardingCard.onboardingType,
 | 
						||
        });
 | 
						||
        this.renderLinkPreviewPanel(win, url, "onboarding");
 | 
						||
      }
 | 
						||
    );
 | 
						||
    onboardingCard.addEventListener("LinkPreviewCard:onboardingClose", () => {
 | 
						||
      panel.hidePopup();
 | 
						||
    });
 | 
						||
 | 
						||
    panel.append(onboardingCard);
 | 
						||
    panel.openPopupNearMouse();
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Initializes a new link preview panel or resets an existing one.
 | 
						||
   * Ensures the panel is ready to display content.
 | 
						||
   *
 | 
						||
   * @param {Window} win - The browser window context.
 | 
						||
   * @param {string} cardType - The trigger source for the panel initialization
 | 
						||
   * @returns {Panel} The initialized or reset panel element.
 | 
						||
   */
 | 
						||
  initOrResetPreviewPanel(win, cardType) {
 | 
						||
    const doc = win.document;
 | 
						||
    let panel = doc.getElementById(this.linkPreviewPanelId);
 | 
						||
 | 
						||
    // If it already exists, hide any open popup and clear out old content.
 | 
						||
    if (panel) {
 | 
						||
      // Transitioning from onboarding reuses the panel without hiding.
 | 
						||
      if (panel.cardType == "linkpreview") {
 | 
						||
        panel.hidePopup();
 | 
						||
      }
 | 
						||
      panel.replaceChildren();
 | 
						||
    } else {
 | 
						||
      panel = doc
 | 
						||
        .getElementById("mainPopupSet")
 | 
						||
        .appendChild(doc.createXULElement("panel"));
 | 
						||
      panel.className = "panel-no-padding";
 | 
						||
      panel.id = this.linkPreviewPanelId;
 | 
						||
      panel.setAttribute("noautofocus", true);
 | 
						||
      panel.setAttribute("type", "arrow");
 | 
						||
      panel.style.width = "362px";
 | 
						||
      panel.style.setProperty("--og-padding", "var(--space-xlarge)");
 | 
						||
      // Match the radius of the image extended out by the padding.
 | 
						||
      panel.style.setProperty(
 | 
						||
        "--panel-border-radius",
 | 
						||
        "calc(var(--border-radius-small) + var(--og-padding))"
 | 
						||
      );
 | 
						||
 | 
						||
      const openPopup = () => {
 | 
						||
        const { _x: x, _y: y } = win.MousePosTracker;
 | 
						||
        // Open near the mouse offsetting so link in the card can be clicked.
 | 
						||
        panel.openPopup(doc.documentElement, "overlap", x - 20, y - 160);
 | 
						||
        panel.openTime = Date.now();
 | 
						||
      };
 | 
						||
      panel.openPopupNearMouse = openPopup;
 | 
						||
 | 
						||
      // Add a single, unified popuphidden listener once on panel init. This
 | 
						||
      // listener will check panel.cardType to determine the correct Glean call.
 | 
						||
      panel.addEventListener("popuphidden", () => {
 | 
						||
        if (panel.cardType === "onboarding") {
 | 
						||
          Glean.genaiLinkpreview.onboardingCard.record({
 | 
						||
            action: "close",
 | 
						||
            type: panel.onboardingType,
 | 
						||
          });
 | 
						||
        } else if (panel.cardType === "linkpreview") {
 | 
						||
          const tabValue = this._getTabContextValue(win);
 | 
						||
          Glean.genaiLinkpreview.cardClose.record({
 | 
						||
            duration: Date.now() - panel.openTime,
 | 
						||
            tab: tabValue,
 | 
						||
          });
 | 
						||
        }
 | 
						||
      });
 | 
						||
    }
 | 
						||
    panel.cardType = cardType;
 | 
						||
    return panel;
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles long press events.
 | 
						||
   *
 | 
						||
   * @param {MouseEvent} event - The mouse related events to be processed.
 | 
						||
   */
 | 
						||
  _onPressEvent(event) {
 | 
						||
    if (!lazy.longPress) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    // Check for the start of a long unmodified primary button press on a link.
 | 
						||
    const win = event.currentTarget;
 | 
						||
    const stateObject = this._windowStates.get(win);
 | 
						||
    if (
 | 
						||
      event.type == "mousedown" &&
 | 
						||
      !event.button &&
 | 
						||
      !event.altKey &&
 | 
						||
      !event.ctrlKey &&
 | 
						||
      !event.metaKey &&
 | 
						||
      !event.shiftKey &&
 | 
						||
      stateObject.overLink
 | 
						||
    ) {
 | 
						||
      // Detect events to cancel the long press.
 | 
						||
      win.addEventListener("dragstart", this, true);
 | 
						||
      win.addEventListener("mouseup", this, true);
 | 
						||
 | 
						||
      // Show preview after a delay if not cancelled.
 | 
						||
      const timer = win.setTimeout(() => {
 | 
						||
        this.cancelLongPress();
 | 
						||
        this.renderLinkPreviewPanel(win, stateObject.overLink, "long_press");
 | 
						||
      }, lazy.longPressMs);
 | 
						||
 | 
						||
      // Provide a way to clean up.
 | 
						||
      this.cancelLongPress = () => {
 | 
						||
        win.clearTimeout(timer);
 | 
						||
        win.removeEventListener("dragstart", this, true);
 | 
						||
        win.removeEventListener("mouseup", this, true);
 | 
						||
        this.cancelLongPress = null;
 | 
						||
      };
 | 
						||
    } else {
 | 
						||
      this.cancelLongPress?.();
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Checks if the user's region is supported for key points generation.
 | 
						||
   *
 | 
						||
   * @returns {boolean} True if the region is supported, false otherwise.
 | 
						||
   */
 | 
						||
  _isRegionSupported() {
 | 
						||
    const disallowedRegions = lazy.noKeyPointsRegions
 | 
						||
      .split(",")
 | 
						||
      .map(region => region.trim().toUpperCase());
 | 
						||
 | 
						||
    const userRegion = lazy.Region.home?.toUpperCase();
 | 
						||
    return !disallowedRegions.includes(userRegion);
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Creates an Open Graph (OG) card using meta information from the page.
 | 
						||
   *
 | 
						||
   * @param {Document} doc - The document object where the OG card will be
 | 
						||
   * created.
 | 
						||
   * @param {object} pageData - An object containing page data, including meta
 | 
						||
   * tags and article information.
 | 
						||
   * @param {object} [pageData.article] - Optional article-specific data.
 | 
						||
   * @param {object} [pageData.metaInfo] - Optional meta tag key-value pairs.
 | 
						||
   * @returns {Element} A DOM element representing the OG card.
 | 
						||
   */
 | 
						||
  createOGCard(doc, pageData) {
 | 
						||
    const ogCard = doc.createElement("link-preview-card");
 | 
						||
    ogCard.style.width = "100%";
 | 
						||
    ogCard.pageData = pageData;
 | 
						||
 | 
						||
    ogCard.optin = lazy.optin;
 | 
						||
    ogCard.collapsed = lazy.collapsed;
 | 
						||
    ogCard.regionSupported = this._isRegionSupported();
 | 
						||
 | 
						||
    // Reflect the shared download progress to this preview.
 | 
						||
    const updateProgress = () => {
 | 
						||
      ogCard.progress = this.progress;
 | 
						||
      // If we are still downloading, update the progress again.
 | 
						||
      if (this.progress >= 0) {
 | 
						||
        doc.ownerGlobal.setTimeout(
 | 
						||
          () => ogCard.isConnected && updateProgress(),
 | 
						||
          250
 | 
						||
        );
 | 
						||
      }
 | 
						||
    };
 | 
						||
    updateProgress();
 | 
						||
 | 
						||
    if (!this._isRegionSupported()) {
 | 
						||
      // Region not supported, just don't show key points section
 | 
						||
      return ogCard;
 | 
						||
    }
 | 
						||
 | 
						||
    // Generate key points if we have content, language and configured for any
 | 
						||
    // language or restricted.
 | 
						||
    if (
 | 
						||
      pageData.article.textContent &&
 | 
						||
      pageData.article.detectedLanguage &&
 | 
						||
      (!lazy.allowedLanguages ||
 | 
						||
        lazy.allowedLanguages
 | 
						||
          .split(",")
 | 
						||
          .includes(pageData.article.detectedLanguage))
 | 
						||
    ) {
 | 
						||
      this.generateKeyPoints(ogCard);
 | 
						||
    } else {
 | 
						||
      ogCard.isMissingDataErrorState = true;
 | 
						||
    }
 | 
						||
 | 
						||
    return ogCard;
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Generate AI key points for card.
 | 
						||
   *
 | 
						||
   * @param {LinkPreviewCard} ogCard to add key points
 | 
						||
   * @param {boolean} _retry Indicates whether to retry the operation.
 | 
						||
   */
 | 
						||
  async generateKeyPoints(ogCard, _retry = false) {
 | 
						||
    // Prevent keypoints if user not opt-in to link preview or user is set
 | 
						||
    // keypoints to be collapsed.
 | 
						||
    if (!lazy.optin || lazy.collapsed) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    // Support prefetching without a card by mocking expected properties.
 | 
						||
    let outcome = ogCard ? "success" : "prefetch";
 | 
						||
    if (!ogCard) {
 | 
						||
      ogCard = { addKeyPoint() {}, isConnected: true, keyPoints: [] };
 | 
						||
    }
 | 
						||
 | 
						||
    const startTime = Date.now();
 | 
						||
    ogCard.generating = true;
 | 
						||
 | 
						||
    // Ensure sequential AI processing to reduce memory usage by passing our
 | 
						||
    // promise to the next request before waiting on the previous.
 | 
						||
    const previous = this.lastRequest;
 | 
						||
    const { promise, resolve } = Promise.withResolvers();
 | 
						||
    this.lastRequest = promise;
 | 
						||
    await previous;
 | 
						||
    const delay = Date.now() - startTime;
 | 
						||
 | 
						||
    // No need to generate if already removed.
 | 
						||
    if (!ogCard.isConnected) {
 | 
						||
      resolve();
 | 
						||
      Glean.genaiLinkpreview.generate.record({
 | 
						||
        delay,
 | 
						||
        outcome: "removed",
 | 
						||
      });
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    let download, latency;
 | 
						||
    try {
 | 
						||
      await lazy.LinkPreviewModel.generateTextAI(
 | 
						||
        ogCard.pageData?.article.textContent ?? "",
 | 
						||
        {
 | 
						||
          onDownload: (downloading, percentage) => {
 | 
						||
            // Initial percentage is NaN, so set to 0.
 | 
						||
            percentage = isNaN(percentage) ? 0 : percentage;
 | 
						||
            // Use the percentage while downloading, otherwise disable with -1.
 | 
						||
            this.progress = downloading ? percentage : -1;
 | 
						||
            ogCard.progress = this.progress;
 | 
						||
            download = Date.now() - startTime;
 | 
						||
          },
 | 
						||
          onError: error => {
 | 
						||
            console.error(error);
 | 
						||
            outcome = error;
 | 
						||
            ogCard.generationError = error;
 | 
						||
          },
 | 
						||
          onText: text => {
 | 
						||
            // Clear waiting in case a different generate handled download.
 | 
						||
            ogCard.showWait = false;
 | 
						||
            ogCard.addKeyPoint(text);
 | 
						||
            latency = latency ?? Date.now() - startTime;
 | 
						||
          },
 | 
						||
        }
 | 
						||
      );
 | 
						||
    } finally {
 | 
						||
      resolve();
 | 
						||
      ogCard.generating = false;
 | 
						||
      Glean.genaiLinkpreview.generate.record({
 | 
						||
        delay,
 | 
						||
        download,
 | 
						||
        latency,
 | 
						||
        outcome,
 | 
						||
        sentences: ogCard.keyPoints.length,
 | 
						||
        time: Date.now() - startTime,
 | 
						||
      });
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles key points generation requests from different user actions.
 | 
						||
   * This is a shared handler for both retry and initial generation events.
 | 
						||
   * Resets error states and triggers key points generation.
 | 
						||
   *
 | 
						||
   * @param {LinkPreviewCard} ogCard - The card element to generate key points for
 | 
						||
   * @private
 | 
						||
   */
 | 
						||
  _handleKeyPointsGenerationEvent(ogCard) {
 | 
						||
    // Reset error states
 | 
						||
    ogCard.isMissingDataErrorState = false;
 | 
						||
    ogCard.isGenerationErrorState = false;
 | 
						||
 | 
						||
    this.generateKeyPoints(ogCard, true);
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Renders the link preview panel at the specified coordinates.
 | 
						||
   *
 | 
						||
   * @param {Window} win - The browser window context.
 | 
						||
   * @param {string} url - The URL of the link to be previewed.
 | 
						||
   * @param {string} source - Optional trigging behavior.
 | 
						||
   */
 | 
						||
  async renderLinkPreviewPanel(win, url, source = "shortcut") {
 | 
						||
    // If link preview is used once not via onboarding, stop onboarding.
 | 
						||
    if (source !== "onboarding") {
 | 
						||
      const maxFreq = lazy.onboardingMaxShowFreq;
 | 
						||
      // Fill the times array up to maxFreq with an array of 0 timestamps.
 | 
						||
      Services.prefs.setStringPref(
 | 
						||
        "browser.ml.linkPreview.onboardingTimes",
 | 
						||
        [...lazy.onboardingTimes, ...Array(maxFreq).fill("0")].slice(0, maxFreq)
 | 
						||
      );
 | 
						||
    }
 | 
						||
 | 
						||
    // Transition from onboarding to preview content with transparency.
 | 
						||
    const doc = win.document;
 | 
						||
    let panel = doc.getElementById(this.linkPreviewPanelId);
 | 
						||
    if (source == "onboarding") {
 | 
						||
      panel.style.setProperty("opacity", "0");
 | 
						||
    }
 | 
						||
 | 
						||
    // Get tab context value for telemetry
 | 
						||
    const tabValue = this._getTabContextValue(win);
 | 
						||
 | 
						||
    // Reuse or initialize panel.
 | 
						||
    if (panel && panel.previewUrl == url) {
 | 
						||
      if (panel.state == "closed") {
 | 
						||
        panel.openPopupNearMouse();
 | 
						||
        Glean.genaiLinkpreview.start.record({
 | 
						||
          cached: true,
 | 
						||
          source,
 | 
						||
          tab: tabValue,
 | 
						||
        });
 | 
						||
      }
 | 
						||
      return;
 | 
						||
    }
 | 
						||
    panel = this.initOrResetPreviewPanel(win, "linkpreview");
 | 
						||
    panel.previewUrl = url;
 | 
						||
 | 
						||
    Glean.genaiLinkpreview.start.record({
 | 
						||
      cached: false,
 | 
						||
      source,
 | 
						||
      tab: tabValue,
 | 
						||
    });
 | 
						||
 | 
						||
    // TODO we want to immediately add a card as a placeholder to have UI be
 | 
						||
    // more responsive while we wait on fetching page data.
 | 
						||
    const browsingContext = win.browsingContext;
 | 
						||
    const actor = browsingContext.currentWindowGlobal.getActor("LinkPreview");
 | 
						||
    const fetchTime = Date.now();
 | 
						||
    const pageData = await actor.fetchPageData(url);
 | 
						||
    // Skip updating content if we've moved on to showing something else.
 | 
						||
    const skipped = pageData.url != panel.previewUrl;
 | 
						||
    Glean.genaiLinkpreview.fetch.record({
 | 
						||
      description: !!pageData.meta.description,
 | 
						||
      image: !!pageData.meta.imageUrl,
 | 
						||
      length:
 | 
						||
        Math.round((pageData.article.textContent?.length ?? 0) * 0.01) * 100,
 | 
						||
      outcome: pageData.error?.result ?? "success",
 | 
						||
      sitename: !!pageData.article.siteName,
 | 
						||
      skipped,
 | 
						||
      tab: tabValue,
 | 
						||
      time: Date.now() - fetchTime,
 | 
						||
      title: !!pageData.meta.title,
 | 
						||
    });
 | 
						||
    if (skipped) {
 | 
						||
      return;
 | 
						||
    }
 | 
						||
 | 
						||
    const ogCard = this.createOGCard(doc, pageData);
 | 
						||
    panel.append(ogCard);
 | 
						||
    ogCard.addEventListener("LinkPreviewCard:dismiss", event => {
 | 
						||
      panel.hidePopup();
 | 
						||
      Glean.genaiLinkpreview.cardLink.record({
 | 
						||
        key_points: !lazy.collapsed,
 | 
						||
        source: event.detail,
 | 
						||
        tab: tabValue,
 | 
						||
      });
 | 
						||
    });
 | 
						||
 | 
						||
    ogCard.addEventListener("LinkPreviewCard:retry", _event => {
 | 
						||
      this._handleKeyPointsGenerationEvent(ogCard, "retry");
 | 
						||
      Glean.genaiLinkpreview.cardLink.record({
 | 
						||
        key_points: !lazy.collapsed,
 | 
						||
        source: "retry",
 | 
						||
        tab: tabValue,
 | 
						||
      });
 | 
						||
    });
 | 
						||
 | 
						||
    ogCard.addEventListener("LinkPreviewCard:generate", _event => {
 | 
						||
      if (ogCard.keyPoints?.length || ogCard.generating) {
 | 
						||
        return;
 | 
						||
      }
 | 
						||
      this._handleKeyPointsGenerationEvent(ogCard, "generate");
 | 
						||
    });
 | 
						||
 | 
						||
    // Make sure panel is visible if previously showing onboarding.
 | 
						||
    panel.style.setProperty("opacity", "1");
 | 
						||
    if (source !== "onboarding") {
 | 
						||
      panel.openPopupNearMouse();
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Determines whether to process or cancel the link preview based on the current state.
 | 
						||
   * If a URL is available and the keyboard combination is active, it processes the link preview.
 | 
						||
   * Otherwise, it cancels the link preview.
 | 
						||
   *
 | 
						||
   * @param {Window} win - The window context in which the link preview may occur.
 | 
						||
   */
 | 
						||
  _maybeLinkPreview(win) {
 | 
						||
    const stateObject = this._windowStates.get(win);
 | 
						||
    const url = stateObject.overLink;
 | 
						||
    // Render preview if we have url, keyboard combo and not recently typing.
 | 
						||
    // Ignore check intends to avoid cases where mouse happens to be over a
 | 
						||
    // link, e.g., after navigating then using an in-page keyboard shortcut or
 | 
						||
    // typing characters that require shift.
 | 
						||
    if (
 | 
						||
      url &&
 | 
						||
      this.keyboardComboActive &&
 | 
						||
      Date.now() - this.overLinkTime <= lazy.ignoreMs &&
 | 
						||
      Date.now() - this.recentTyping >= lazy.recentTypingMs
 | 
						||
    ) {
 | 
						||
      this.renderLinkPreviewPanel(win, url, this.keyboardComboActive);
 | 
						||
    }
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Handles the link preview context menu click using the provided URL
 | 
						||
   * and nsContextMenu, prompting the link preview panel to open.
 | 
						||
   *
 | 
						||
   * @param {string} url - The URL of the link to be previewed.
 | 
						||
   * @param {object} nsContextMenu - The context menu object containing browser information.
 | 
						||
   */
 | 
						||
  async handleContextMenuClick(url, nsContextMenu) {
 | 
						||
    let win = nsContextMenu.browser.ownerGlobal;
 | 
						||
    this.renderLinkPreviewPanel(win, url, "context");
 | 
						||
  },
 | 
						||
 | 
						||
  /**
 | 
						||
   * Updates the Glean metric for active shortcuts.
 | 
						||
   * This metric is a comma-separated string of active shortcut types.
 | 
						||
   *
 | 
						||
   * @private
 | 
						||
   */
 | 
						||
  _updateShortcutMetric() {
 | 
						||
    const activeShortcuts = [];
 | 
						||
    if (lazy.shift) {
 | 
						||
      activeShortcuts.push("shift");
 | 
						||
    }
 | 
						||
    if (lazy.shiftAlt) {
 | 
						||
      activeShortcuts.push("shift_alt");
 | 
						||
    }
 | 
						||
    if (lazy.longPress) {
 | 
						||
      activeShortcuts.push("long_press");
 | 
						||
    }
 | 
						||
    Glean.genaiLinkpreview.shortcut.set(activeShortcuts.join(","));
 | 
						||
  },
 | 
						||
};
 |