diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/chat/content/conversation-browser.js | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/chat/content/conversation-browser.js')
-rw-r--r-- | comm/chat/content/conversation-browser.js | 906 |
1 files changed, 906 insertions, 0 deletions
diff --git a/comm/chat/content/conversation-browser.js b/comm/chat/content/conversation-browser.js new file mode 100644 index 0000000000..baa7f57447 --- /dev/null +++ b/comm/chat/content/conversation-browser.js @@ -0,0 +1,906 @@ +/* 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", + }); +} |