/* 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/. */

"use strict";

/* global MozXULElement */

// Wrap in a block to prevent leaking to window scope.
{
  const LazyModules = {};
  ChromeUtils.defineESModuleGetters(LazyModules, {
    cleanupImMarkup: "resource:///modules/imContentSink.sys.mjs",
    getCurrentTheme: "resource:///modules/imThemes.sys.mjs",
    getDocumentFragmentFromHTML: "resource:///modules/imThemes.sys.mjs",
    getHTMLForMessage: "resource:///modules/imThemes.sys.mjs",
    initHTMLDocument: "resource:///modules/imThemes.sys.mjs",
    insertHTMLForMessage: "resource:///modules/imThemes.sys.mjs",
    isNextMessage: "resource:///modules/imThemes.sys.mjs",
    wasNextMessage: "resource:///modules/imThemes.sys.mjs",
    replaceHTMLForMessage: "resource:///modules/imThemes.sys.mjs",
    removeMessage: "resource:///modules/imThemes.sys.mjs",
    serializeSelection: "resource:///modules/imThemes.sys.mjs",
    smileTextNode: "resource:///modules/imSmileys.sys.mjs",
  });

  (function () {
    // <browser> is lazily set up through setElementCreationCallback,
    // i.e. put into customElements the first time it's really seen.
    // Create a fake to ensure browser exists in customElements, since otherwise
    // we can't extend it. Then make sure this fake doesn't stay around.
    if (!customElements.get("browser")) {
      delete document.createXULElement("browser");
    }
  })();

  /**
   * The chat conversation browser, i.e. the main content on the chat tab.
   *
   * @augments {MozBrowser}
   */
  class MozConversationBrowser extends customElements.get("browser") {
    constructor() {
      super();

      this._conv = null;

      // Make sure to load URLs externally.
      this.addEventListener("click", event => {
        // Right click should open the context menu.
        if (event.button == 2) {
          return;
        }

        // The 'click' event is fired even when the link is
        // activated with the keyboard.

        // The event target may be a descendant of the actual link.
        let url;
        for (let elem = event.target; elem; elem = elem.parentNode) {
          if (HTMLAnchorElement.isInstance(elem)) {
            url = elem.href;
            if (url) {
              break;
            }
          }
        }
        if (!url) {
          return;
        }

        let uri = Services.io.newURI(url);

        // http and https are the only schemes that are both
        // allowed by our IM filters and exposed.
        if (!uri.schemeIs("http") && !uri.schemeIs("https")) {
          return;
        }

        event.preventDefault();
        event.stopPropagation();

        // loadURI can throw if the default browser is misconfigured.
        Cc["@mozilla.org/uriloader/external-protocol-service;1"]
          .getService(Ci.nsIExternalProtocolService)
          .loadURI(uri);
      });

      this.addEventListener("keypress", event => {
        switch (event.keyCode) {
          case KeyEvent.DOM_VK_PAGE_UP: {
            if (event.shiftKey) {
              this.contentWindow.scrollByPages(-1);
            } else if (event.altKey) {
              this.scrollToPreviousSection();
            }
            break;
          }
          case KeyEvent.DOM_VK_PAGE_DOWN: {
            if (event.shiftKey) {
              this.contentWindow.scrollByPages(1);
            } else if (event.altKey) {
              this.scrollToNextSection();
            }
            break;
          }
          case KeyEvent.DOM_VK_HOME: {
            this.scrollToPreviousSection();
            event.preventDefault();
            break;
          }
          case KeyEvent.DOM_VK_END: {
            this.scrollToNextSection();
            event.preventDefault();
            break;
          }
        }
      });
    }

    connectedCallback() {
      if (this.hasConnected) {
        return;
      }
      this.hasConnected = true;

      super.connectedCallback();

      this._theme = null;

      this.autoCopyEnabled = false;

      this.magicCopyPref =
        "messenger.conversations.selections.magicCopyEnabled";

      this.magicCopyInitialized = false;

      this._destroyed = false;

      // Makes the chat browser scroll to the bottom automatically when we append
      // a new message. This behavior gets disabled when the user scrolls up to
      // look at the history, and we re-enable it when the user scrolls to
      // (within 10px) of the bottom.
      this._convScrollEnabled = true;

      this._textModifiers = [LazyModules.smileTextNode];

      // These variables are reset in onStateChange:
      this._lastMessage = null;
      this._lastMessageIsContext = true;
      this._firstNonContextElt = null;
      this._messageDisplayPending = false;
      this._pendingMessages = [];
      this._nextPendingMessageIndex = 0;
      this._pendingMessagesDisplayed = 0;
      this._displayPendingMessagesCalls = 0;
      this._sessions = [];

      this.progressBar = null;

      this.addEventListener("scroll", this.browserScroll);
      this.addEventListener("resize", this.browserResize);

      // @implements {nsIObserver}
      this.prefObserver = (subject, topic, data) => {
        if (this.magicCopyEnabled) {
          this.enableMagicCopy();
        } else {
          this.disableMagicCopy();
        }
      };

      // @implements {nsIController}
      this.copyController = {
        supportsCommand(command) {
          return command == "cmd_copy" || command == "cmd_cut";
        },
        isCommandEnabled: command => {
          return (
            command == "cmd_copy" &&
            !this.contentWindow.getSelection().isCollapsed
          );
        },
        doCommand: command => {
          let selection = this.contentWindow.getSelection();
          if (selection.isCollapsed) {
            return;
          }

          Cc["@mozilla.org/widget/clipboardhelper;1"]
            .getService(Ci.nsIClipboardHelper)
            .copyString(LazyModules.serializeSelection(selection));
        },
        onEvent(command) {},
        QueryInterface: ChromeUtils.generateQI(["nsIController"]),
      };

      // @implements {nsISelectionListener}
      this.chatSelectionListener = {
        notifySelectionChanged(document, selection, reason) {
          if (
            !(
              reason & Ci.nsISelectionListener.MOUSEUP_REASON ||
              reason & Ci.nsISelectionListener.SELECTALL_REASON ||
              reason & Ci.nsISelectionListener.KEYPRESS_REASON
            )
          ) {
            // We are still dragging, don't bother with the selection.
            return;
          }

          Cc["@mozilla.org/widget/clipboardhelper;1"]
            .getService(Ci.nsIClipboardHelper)
            .copyStringToClipboard(
              LazyModules.serializeSelection(selection),
              Ci.nsIClipboard.kSelectionClipboard
            );
        },
        QueryInterface: ChromeUtils.generateQI(["nsISelectionListener"]),
      };
    }

    init(conversation) {
      // Magic Copy may be initialized if the convbrowser is already
      // displaying a conversation.
      this.uninitMagicCopy();

      this._conv = conversation;

      // init is called when the message style preview is
      // reloaded so we need to reset _theme.
      this._theme = null;

      // Prevent ongoing asynchronous message display from continuing.
      this._messageDisplayPending = false;

      this.addEventListener(
        "load",
        () => {
          LazyModules.initHTMLDocument(
            this._conv,
            this.theme,
            this.contentDocument
          );

          this._exposeMethodsToContent();
          this.initMagicCopy();

          // We need to reset these variables here to avoid a race
          // condition if we are starting to display a new conversation
          // but the display of the previous conversation wasn't finished.
          // This can happen if the user quickly changes the selected
          // conversation in the log viewer.
          this._lastMessage = null;
          this._lastMessageIsContext = true;
          this._firstNonContextElt = null;
          this._messageDisplayPending = false;
          this._pendingMessages = [];
          this._nextPendingMessageIndex = 0;
          this._pendingMessagesDisplayed = 0;
          this._displayPendingMessagesCalls = 0;
          this._sessions = [];
          if (this.progressBar) {
            this.progressBar.hidden = true;
          }

          this.onChatNodeContentLoad = this.onContentElementLoad.bind(this);
          this.contentChatNode.addEventListener(
            "load",
            this.onChatNodeContentLoad,
            true
          );

          // Notify observers to get the conversation shown.
          Services.obs.notifyObservers(this, "conversation-loaded");
        },
        {
          once: true,
          capture: true,
        }
      );
      this.loadURI(Services.io.newURI("chrome://chat/content/conv.html"), {
        triggeringPrincipal:
          Services.scriptSecurityManager.getSystemPrincipal(),
      });
    }

    get theme() {
      return this._theme || (this._theme = LazyModules.getCurrentTheme());
    }

    get contentDocument() {
      return this.webNavigation.document;
    }

    get contentChatNode() {
      return this.contentDocument.getElementById("Chat");
    }

    get magicCopyEnabled() {
      return Services.prefs.getBoolPref(this.magicCopyPref);
    }

    enableMagicCopy() {
      this.contentWindow.controllers.insertControllerAt(0, this.copyController);
      this.autoCopyEnabled =
        Services.clipboard.isClipboardTypeSupported(
          Services.clipboard.kSelectionClipboard
        ) && Services.prefs.getBoolPref("clipboard.autocopy");
      if (this.autoCopyEnabled) {
        let selection = this.contentWindow.getSelection();
        if (selection) {
          selection.addSelectionListener(this.chatSelectionListener);
        }
      }
    }

    disableMagicCopy() {
      this.contentWindow.controllers.removeController(this.copyController);
      if (this.autoCopyEnabled) {
        let selection = this.contentWindow.getSelection();
        if (selection) {
          selection.removeSelectionListener(this.chatSelectionListener);
        }
      }
    }

    initMagicCopy() {
      if (this.magicCopyInitialized) {
        return;
      }
      Services.prefs.addObserver(this.magicCopyPref, this.prefObserver);
      this.magicCopyInitialized = true;
      if (this.magicCopyEnabled) {
        this.enableMagicCopy();
      }
    }

    uninitMagicCopy() {
      if (!this.magicCopyInitialized) {
        return;
      }
      Services.prefs.removeObserver(this.magicCopyPref, this.prefObserver);
      if (this.magicCopyEnabled) {
        this.disableMagicCopy();
      }
      this.magicCopyInitialized = false;
    }

    destroy() {
      super.destroy();
      if (this._destroyed) {
        return;
      }
      this._destroyed = true;
      this._messageDisplayPending = false;

      this.uninitMagicCopy();

      if (this.contentChatNode) {
        // Remove the listener only if the conversation was initialized.
        this.contentChatNode.removeEventListener(
          "load",
          this.onChatNodeContentLoad,
          true
        );
      }
    }

    _updateConvScrollEnabled() {
      // Enable auto-scroll if the scrollbar is at the bottom.
      let body = this.contentDocument.querySelector("body");
      this._convScrollEnabled =
        body.scrollHeight <= body.scrollTop + body.clientHeight + 10;
      return this._convScrollEnabled;
    }

    convScrollEnabled() {
      return this._convScrollEnabled || this._updateConvScrollEnabled();
    }

    _scrollToElement(aElt) {
      aElt.scrollIntoView(true);
      this._scrollingIntoView = true;
    }

    _exposeMethodsToContent() {
      // Expose scrollToElement and convScrollEnabled to the message styles.
      this.contentWindow.scrollToElement = this._scrollToElement.bind(this);
      this.contentWindow.convScrollEnabled = this.convScrollEnabled.bind(this);
    }

    addTextModifier(aModifier) {
      if (!this._textModifiers.includes(aModifier)) {
        this._textModifiers.push(aModifier);
      }
    }

    set isActive(value) {
      if (!value && !this.browsingContext) {
        return;
      }
      this.browsingContext.isActive = value;
      if (value && this._pendingMessages.length) {
        this.startDisplayingPendingMessages(false);
      }
    }

    appendMessage(aMsg, aContext, aFirstUnread) {
      this._pendingMessages.push({
        msg: aMsg,
        context: aContext,
        firstUnread: aFirstUnread,
      });
      if (this.browsingContext.isActive) {
        this.startDisplayingPendingMessages(true);
      }
    }

    /**
     * Replace an existing message in the conversation based on the remote ID.
     *
     * @param {imIMessage} msg - Message to use as replacement.
     */
    replaceMessage(msg) {
      if (!msg.remoteId) {
        // No remote id, nothing existing to replace.
        return;
      }
      if (this._messageDisplayPending || this._pendingMessages.length) {
        let pendingIndex = this._pendingMessages.findIndex(
          ({ msg: pendingMsg }) => pendingMsg.remoteId === msg.remoteId
        );
        if (
          pendingIndex > -1 &&
          pendingIndex >= this._nextPendingMessageIndex
        ) {
          this._pendingMessages[pendingIndex].msg = msg;
        }
      }
      if (this.browsingContext.isActive) {
        msg.message = this.prepareMessageContent(msg);
        const isNext = LazyModules.wasNextMessage(msg, this.contentDocument);
        const htmlMessage = LazyModules.getHTMLForMessage(
          msg,
          this.theme,
          isNext,
          false
        );
        let ruler = this.contentDocument.getElementById("unread-ruler");
        if (ruler?._originalMsg?.remoteId === msg.remoteId) {
          ruler._originalMsg = msg;
          ruler.nextMsgHtml = htmlMessage;
        }
        LazyModules.replaceHTMLForMessage(
          msg,
          htmlMessage,
          this.contentDocument,
          isNext
        );
      }
      if (this._lastMessage?.remoteId === msg.remoteId) {
        this._lastMessage = msg;
      }
    }

    /**
     * Remove an existing message in the conversation based on the remote ID.
     *
     * @param {string} remoteId - Remote ID of the message to remove.
     */
    removeMessage(remoteId) {
      if (this.browsingContext.isActive) {
        LazyModules.removeMessage(remoteId, this.contentDocument);
      }
      if (this._lastMessage?.remoteId === remoteId) {
        // Reset last message info if we removed the last message.
        this._lastMessage = null;
      }
    }

    startDisplayingPendingMessages(delayed) {
      if (this._messageDisplayPending) {
        return;
      }
      this._messageDisplayPending = true;
      this.contentWindow.messageInsertPending = true;
      if (delayed) {
        requestIdleCallback(this.displayPendingMessages.bind(this));
      } else {
        // 200ms here is a generous amount of time. The conversation switch
        // should take no more than 100ms to feel 'immediate', but the perceived
        // performance if we flicker is likely even worse than having a barely
        // perceptible delay.
        let deadline = Cu.now() + 200;
        this.displayPendingMessages({
          timeRemaining() {
            return deadline - Cu.now();
          },
        });
      }
    }

    // getNextPendingMessage and getPendingMessagesCount are the
    // only 2 methods accessing the this._pendingMessages array
    // directly during the chunked display of messages. It is
    // possible to override these 2 methods to replace the array
    // with something else. The log viewer for example uses an
    // enumerator that creates message objects lazily to avoid
    // jank when displaying lots of messages.
    getNextPendingMessage() {
      let length = this._pendingMessages.length;
      if (this._nextPendingMessageIndex == length) {
        return null;
      }

      let result = this._pendingMessages[this._nextPendingMessageIndex++];

      if (this._nextPendingMessageIndex == length) {
        this._pendingMessages = [];
        this._nextPendingMessageIndex = 0;
      }

      return result;
    }

    getPendingMessagesCount() {
      return this._pendingMessages.length;
    }

    displayPendingMessages(timing) {
      if (!this._messageDisplayPending) {
        return;
      }

      let max = this.getPendingMessagesCount();
      do {
        // One message takes less than 2ms on average.
        let msg = this.getNextPendingMessage();
        if (!msg) {
          break;
        }
        this.displayMessage(
          msg.msg,
          msg.context,
          ++this._pendingMessagesDisplayed < max,
          msg.firstUnread
        );
      } while (timing.timeRemaining() > 2);

      let event = document.createEvent("UIEvents");
      event.initUIEvent("MessagesDisplayed", false, false, window, 0);
      if (this._pendingMessagesDisplayed < max) {
        if (this.progressBar) {
          // Show progress bar if after the third call (ca. 120ms)
          // less than half the messages have been displayed.
          if (
            ++this._displayPendingMessagesCalls > 2 &&
            max > 2 * this._pendingMessagesDisplayed
          ) {
            this.progressBar.hidden = false;
          }
          this.progressBar.max = max;
          this.progressBar.value = this._pendingMessagesDisplayed;
        }
        requestIdleCallback(this.displayPendingMessages.bind(this));
        this.dispatchEvent(event);
        return;
      }
      this.contentWindow.messageInsertPending = false;
      this._messageDisplayPending = false;
      this._pendingMessagesDisplayed = 0;
      this._displayPendingMessagesCalls = 0;
      if (this.progressBar) {
        this.progressBar.hidden = true;
      }
      this.dispatchEvent(event);
    }

    displayMessage(aMsg, aContext, aNoAutoScroll, aFirstUnread) {
      let doc = this.contentDocument;

      if (aMsg.noLog && aMsg.notification && aMsg.who == "sessionstart") {
        // New session log.
        if (this._lastMessage) {
          let ruler = doc.createElement("hr");
          ruler.className = "sessionstart-ruler";
          this.contentChatNode.appendChild(ruler);
          this._sessions.push(ruler);
          // Close any open bubble.
          this._lastMessage = null;
        }
        // Suppress this message unless it was an error message.
        if (!aMsg.error) {
          return;
        }
      }

      if (aFirstUnread) {
        this.setUnreadRuler();
      }

      aMsg.message = this.prepareMessageContent(aMsg);

      let next =
        (aContext == this._lastMessageIsContext || aMsg.system) &&
        LazyModules.isNextMessage(this.theme, aMsg, this._lastMessage);
      let newElt;
      if (next && aFirstUnread) {
        // If there wasn't an unread ruler, this would be a Next message.
        // Therefore, save that version for later.
        let html = LazyModules.getHTMLForMessage(
          aMsg,
          this.theme,
          next,
          aContext
        );
        let ruler = doc.getElementById("unread-ruler");
        ruler.nextMsgHtml = html;
        ruler._originalMsg = aMsg;

        // Remember where the Next message(s) would have gone.
        let insert = doc.getElementById("insert");
        if (!insert) {
          insert = doc.createElement("div");
          ruler.parentNode.insertBefore(insert, ruler);
        }
        insert.id = "insert-before";

        next = false;
        html = LazyModules.getHTMLForMessage(aMsg, this.theme, next, aContext);
        newElt = LazyModules.insertHTMLForMessage(aMsg, html, doc, next);
        let marker = doc.createElement("div");
        marker.id = "end-of-split-block";
        newElt.parentNode.appendChild(marker);

        // Bracket the place where additional Next messages will be added,
        // if that's not after the end-of-split-block element.
        insert = doc.getElementById("insert");
        if (insert) {
          marker = doc.createElement("div");
          marker.id = "next-messages-start";
          insert.parentNode.insertBefore(marker, insert);
          marker = doc.createElement("div");
          marker.id = "next-messages-end";
          insert.parentNode.insertBefore(marker, insert.nextElementSibling);
        }
      } else {
        let html = LazyModules.getHTMLForMessage(
          aMsg,
          this.theme,
          next,
          aContext
        );
        newElt = LazyModules.insertHTMLForMessage(aMsg, html, doc, next);
      }

      if (!aNoAutoScroll) {
        newElt.getBoundingClientRect(); // avoid ireflow bugs
        if (this.convScrollEnabled()) {
          this._scrollToElement(newElt);
        }
      }
      this._lastElement = newElt;
      this._lastMessage = aMsg;
      if (!aContext && !this._firstNonContextElt && !aMsg.system) {
        this._firstNonContextElt = newElt;
      }
      this._lastMessageIsContext = aContext;
    }

    /**
     * Prepare the message text for display. Transforms plain text formatting
     * and removes any unwanted formatting.
     *
     * @param {imIMessage} message - Raw message.
     * @returns {string} Message content ready for insertion.
     */
    prepareMessageContent(message) {
      let cs = Cc["@mozilla.org/txttohtmlconv;1"].getService(
        Ci.mozITXTToHTMLConv
      );

      // kStructPhrase creates tags for plaintext-markup like *bold*,
      // /italics/, etc. We always use this; the content filter will
      // filter it out if the user does not want styling.
      let csFlags = cs.kStructPhrase;
      // Automatically find and link freetext URLs
      if (!message.noLinkification) {
        csFlags |= cs.kURLs;
      }

      // Right trim before displaying. This removes any OTR related
      // whitespace when the extension isn't enabled.
      let msg = message.displayMessage?.trimRight() ?? "";
      msg = cs
        .scanHTML(msg.replace(/&/g, "FROM-DTD-amp"), csFlags)
        .replace(/FROM-DTD-amp/g, "&");

      return LazyModules.cleanupImMarkup(
        msg.replace(/\r?\n/g, "<br/>"),
        null,
        this._textModifiers
      );
    }

    setUnreadRuler() {
      // Remove any existing ruler (occurs when the window has lost focus).
      this.removeUnreadRuler();

      let ruler = this.contentDocument.createElement("hr");
      ruler.id = "unread-ruler";
      this.contentChatNode.appendChild(ruler);
    }

    removeUnreadRuler() {
      if (this._lastMessage) {
        this._lastMessage.whenRead();
      }

      let doc = this.contentDocument;
      let ruler = doc.getElementById("unread-ruler");
      if (!ruler) {
        return;
      }

      // If a message block was split by the ruler, rejoin it.
      let moveTo = doc.getElementById("insert-before");
      if (moveTo) {
        // Protect an existing insert node.
        let actualInsert = doc.getElementById("insert");
        if (actualInsert) {
          actualInsert.id = "actual-insert";
        }

        // Add first message following the ruler as a Next type message.
        // Replicates the relevant parts of insertHTMLForMessage().
        let range = doc.createRange();
        let moveToParent = moveTo.parentNode;
        range.selectNode(moveToParent);
        // eslint-disable-next-line no-unsanitized/method
        let documentFragment = LazyModules.getDocumentFragmentFromHTML(
          doc,
          ruler.nextMsgHtml
        );
        for (
          let root = documentFragment.firstElementChild;
          root;
          root = root.nextElementSibling
        ) {
          root._originalMsg = ruler._originalMsg;
          root.dataset.remoteId = ruler._originalMsg.remoteId;
        }
        moveToParent.insertBefore(documentFragment, moveTo);

        // If this added an insert node, insert the next messages there.
        let insert = doc.getElementById("insert");
        if (insert) {
          moveTo.remove();
          moveTo = insert;
          moveToParent = moveTo.parentNode;
        }

        // Move remaining messages from the message block following the ruler.
        let nextMessagesStart = doc.getElementById("next-messages-start");
        if (nextMessagesStart) {
          range = doc.createRange();
          range.setStartAfter(nextMessagesStart);
          range.setEndBefore(doc.getElementById("next-messages-end"));
          moveToParent.insertBefore(range.extractContents(), moveTo);
        }
        moveTo.remove();

        // Restore existing insert node.
        if (actualInsert) {
          actualInsert.id = "insert";
        }

        // Delete surplus message block.
        range = doc.createRange();
        range.setStartAfter(ruler);
        range.setEndAfter(doc.getElementById("end-of-split-block"));
        range.deleteContents();
      }
      ruler.remove();
    }

    _getSections() {
      // If a section is displayed below this point, we assume not enough of
      // it is visible, so we must scroll to it.
      // The 3/4 constant is arbitrary, but it has to be greater than 1/2.
      this._maximalSectionOffset = Math.round((this.clientHeight * 3) / 4);

      // Get list of current section elements.
      let sectionElements = [];
      if (this._firstNonContextElt) {
        sectionElements.push(this._firstNonContextElt);
      }
      let ruler = this.contentDocument.getElementById("unread-ruler");
      if (ruler) {
        sectionElements.push(ruler);
      }
      sectionElements = sectionElements.concat(this._sessions);

      // Return ordered array of sections with entries
      // [Y, scrollY such that Y is centered]
      let sections = [];
      let maxY = this.contentWindow.scrollMaxY;
      for (let i = 0; i < sectionElements.length; ++i) {
        let y = sectionElements[i].offsetTop;
        // The section is unnecessary if close to top/bottom of conversation.
        if (y < this._maximalSectionOffset || maxY < y) {
          continue;
        }
        sections.push([y, y - Math.round(this.clientHeight / 2)]);
      }
      sections.sort((a, b) => a[0] - b[0]);
      return sections;
    }

    scrollToPreviousSection() {
      let sections = this._getSections();
      let y = this.contentWindow.scrollY;
      let newY = 0;
      for (let i = sections.length - 1; i >= 0; --i) {
        let section = sections[i];
        if (y > section[0]) {
          newY = section[1];
          break;
        }
      }
      this.contentWindow.scrollTo(0, newY);
    }

    scrollToNextSection() {
      let sections = this._getSections();
      let y = this.contentWindow.scrollY;
      let newY = this.contentWindow.scrollMaxY;
      for (let i = 0; i < sections.length; ++i) {
        let section = sections[i];
        if (y + this._maximalSectionOffset < section[0]) {
          newY = section[1];
          break;
        }
      }
      this.contentWindow.scrollTo(0, newY);
    }

    browserScroll(event) {
      if (this._scrollingIntoView) {
        // We have explicitly requested a scrollIntoView, ignore the event.
        this._scrollingIntoView = false;
        this._lastScrollHeight = this.scrollHeight;
        this._lastScrollWidth = this.scrollWidth;
        return;
      }

      if (
        !("_lastScrollHeight" in this) ||
        this._lastScrollHeight != this.scrollHeight ||
        this._lastScrollWidth != this.scrollWidth
      ) {
        // Ensure scroll events triggered by a change of the
        // content area size (eg. resizing the window or moving the
        // textbox splitter) don't affect the auto-scroll behavior.
        this._lastScrollHeight = this.scrollHeight;
        this._lastScrollWidth = this.scrollWidth;
      }

      // If images higher than one line of text load they will trigger a
      // scroll event, which shouldn't disable auto-scroll while messages
      // are being appended without being scrolled.
      if (this._messageDisplayPending) {
        return;
      }

      // Enable or disable auto-scroll based on the scrollbar position.
      this._updateConvScrollEnabled();
    }

    browserResize(event) {
      if (this._convScrollEnabled && this._lastElement) {
        // The content area was resized and auto-scroll is enabled,
        // make sure the last inserted element is still visible
        this._scrollToElement(this._lastElement);
      }
    }

    onContentElementLoad(event) {
      if (
        event.target.localName == "img" &&
        this._convScrollEnabled &&
        !this._messageDisplayPending &&
        this._lastElement
      ) {
        // An image loaded while auto-scroll is enabled and no further
        // messages are currently being appended. So we need to scroll
        // the last element fully back into view.
        this._scrollToElement(this._lastElement);
      }
    }
  }
  customElements.define("conversation-browser", MozConversationBrowser, {
    extends: "browser",
  });
}
