/* 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/. */ // See chat/content/conversation-browser.js _exposeMethodsToContent /* globals convScrollEnabled, scrollToElement */ /* [pseudo_color, pseudo_background, bubble_borders] */ const elements_lightness = [ [75, 94, 80], [75, 94, 80], [70, 93, 75], [65, 92, 70], [55, 90, 65], [48, 90, 60], [44, 86, 50], [44, 88, 60], [45, 88, 70], [45, 90, 70], [45, 92, 70], [45, 92, 70], [45, 92, 70], [45, 92, 70], [45, 92, 70], [45, 92, 70], [45, 92, 70], [45, 92, 70], [45, 92, 70], [60, 92, 70], [70, 93, 75], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], [75, 94, 80], ]; const bubble_background = "hsl(#, 100%, 97%)"; const bubble_borders = "hsl(#, 100%, #%)"; const pseudo_color = "hsl(#, 100%, #%)"; const pseudo_background = "hsl(#, 100%, #%)"; var alternating = null; function setColors(target) { var senderColor = target.getAttribute("data-senderColor"); if (!senderColor) { return; } var regexp = /color:\s*hsl\(\s*(\d{1,3})\s*,\s*\d{1,3}\%\s*,\s*\d{1,3}\%\s*\)/; var parsed = regexp.exec(senderColor); if (!parsed) { return; } var senderHue = (Math.round(parsed[1] / 10) * 10) % 360; var lightness = elements_lightness[senderHue / 10]; target.style.backgroundColor = bubble_background.replace("#", senderHue); target.style.borderColor = bubble_borders .replace("#", senderHue) .replace("#", lightness[2]); var pseudo = target.getElementsByClassName("pseudo")[0]; pseudo.style.color = pseudo_color .replace("#", senderHue) .replace("#", lightness[0]); pseudo.style.backgroundColor = pseudo_background .replace("#", senderHue) .replace("#", lightness[1]); var div_indicator = target.getElementsByClassName("indicator")[0]; var imageURL = "url('Bitmaps/indicator_" + senderHue; if (target.classList.contains("incoming")) { // getComputedStyle is prohibitively expensive, and we need it only to // know if we are using an alternating variant, so we cache the result. if (alternating === null) { alternating = document.defaultView .getComputedStyle(div_indicator) .backgroundImage.endsWith('_alt.png")') ? "_alt" : ""; } imageURL += alternating; } div_indicator.style.backgroundImage = imageURL + ".png')"; } function prettyPrintTime(aValue, aNoSeconds) { if (aValue < 60 && aNoSeconds) { return ""; } if (aNoSeconds) { aValue -= aValue % 60; } let valuesAndUnits = window.convertTimeUnits(aValue); if (!valuesAndUnits[2]) { valuesAndUnits.splice(2, 2); } return valuesAndUnits.join(" "); } // The "shadow" constant is the minimum acceptable margin-bottom for a bubble // with a shadow, and the minimum spacing between the bubbles of two messages // arriving in the same second. It should match the value of margin-bottom and // box-shadow-bottom for the "bubble" class. const shadow = 3; const coef = 3; const timebeforetextdisplay = 5 * 60; const kRulerMarginTop = 11; const kMsPerMinute = 60 * 1000; const kMsPerHour = 60 * kMsPerMinute; const kMsPerDay = 24 * kMsPerHour; function computeSpace(aInterval) { return Math.round(coef * Math.log(aInterval + 1)); } var lastMessageTimeout; var lastMessageTimeoutTime = -1; /* This function takes care of updating the amount of whitespace * between the last message and the bottom of the conversation area. * When the last message is more than timebeforetextdisplay old, we display * the time in text. To avoid blinking Mac scrollbar and visual distractions * for some very sensitive users, we update the whitespace only when a new * message is displayed or when the user switches between tabs. While the * conversation is visible, this function is called by timers, but we will * only update the time displayed in text (this behavior is obtained by * setting the aUpdateTextOnly parameter to true; otherwise it is omitted). */ function handleLastMessage(aUpdateTextOnly) { if (window.messageInsertPending) { return; } var intervalInMs = Date.now() - lastMsgTime * 1000; var interval = Math.round(intervalInMs / 1000); var p = document.getElementById("lastMessage"); var margin; if (!aUpdateTextOnly) { // Impose a minimum to ensure the last bubble doesn't touch the editbox. margin = computeSpace(Math.max(intervalInMs, 5000) / 1000); } var text = ""; if (interval >= timebeforetextdisplay) { if (!aUpdateTextOnly) { p.style.lineHeight = margin + shadow + "px"; } p.setAttribute("class", "interval"); text = prettyPrintTime(interval, true); margin = 0; } p.textContent = text; if (!aUpdateTextOnly) { p.style.marginTop = margin - shadow + "px"; if (convScrollEnabled()) { scrollToElement(p); } } var next = timebeforetextdisplay * 1000 - intervalInMs; if (next <= 0) { if (intervalInMs > kMsPerDay) { next = kMsPerHour - (intervalInMs % kMsPerHour); } else { next = kMsPerMinute - (intervalInMs % kMsPerMinute); } aUpdateTextOnly = true; } // The setTimeout callbacks are frequently called a few ms early, // but our code prefers being called a little late, so add 20ms. lastMessageTimeoutTime = next + 20; lastMessageTimeout = setTimeout( handleLastMessage, lastMessageTimeoutTime, aUpdateTextOnly ); } var lastMsgTime = 0; function updateLastMsgTime(aMsgTime) { if (aMsgTime > lastMsgTime) { lastMsgTime = aMsgTime; } if (lastMsgTime && lastMessageTimeoutTime != 0 && !document.hidden) { clearTimeout(lastMessageTimeout); setTimeout(handleLastMessage, 0); lastMessageTimeoutTime = 0; } } function visibilityChanged() { if (document.hidden) { clearTimeout(lastMessageTimeout); lastMessageTimeoutTime = -1; } else if (lastMsgTime) { handleLastMessage(); } } function checkNewText(target) { var nicks = target.getElementsByClassName("ib-nick"); for (var i = 0; i < nicks.length; ++i) { var nick = nicks[i]; if (nick.hasAttribute("data-left")) { continue; } var hue = nick.getAttribute("data-nickColor"); var senderHue = (Math.round(hue / 10) * 10) % 360; var lightness = elements_lightness[senderHue / 10]; nick.style.backgroundColor = pseudo_background .replace("#", senderHue) .replace("#", lightness[1]); nick.style.color = pseudo_color .replace("#", senderHue) .replace("#", lightness[0]); nick.style.borderColor = bubble_borders .replace("#", senderHue) .replace("#", lightness[2]); } var msgTime = null; if (target._originalMsg) { msgTime = target._originalMsg.time; } if (target.tagName == "DIV" && target.classList.contains("bubble")) { setColors(target); var prev = target.previousElementSibling; var shouldSetUnreadRuler = prev && prev.id && prev.id == "unread-ruler"; var shouldSetSessionRuler = prev && prev.className && prev.className == "sessionstart-ruler"; // We need an extra pixel of margin at the top to make the margins appear // to be of equal size, since the preceding bubble will have a shadow. var rulerMarginBottom = kRulerMarginTop - 1; if (lastMsgTime && msgTime >= lastMsgTime) { var interval = msgTime - lastMsgTime; var margin = computeSpace(interval); let isTimetext = interval >= timebeforetextdisplay; if (isTimetext) { let p = document.createElement("p"); p.className = "interval"; if (shouldSetSessionRuler) { // Hide the hr and style the time text accordingly instead. prev.classList.remove("sessionstart-ruler"); prev.style.border = "none"; p.classList.add("sessionstart-ruler"); margin += 6; prev = p; } p.style.lineHeight = margin + shadow + "px"; p.style.marginTop = -shadow + "px"; p.textContent = prettyPrintTime(interval); target.parentNode.insertBefore(p, target); margin = 0; } target.style.marginTop = margin + "px"; if (shouldSetUnreadRuler || shouldSetSessionRuler) { if (margin > rulerMarginBottom) { // Set the unread ruler margin so it is constant after margin collapse. // See https://developer.mozilla.org/en/CSS/margin_collapsing rulerMarginBottom -= margin; } if (isTimetext && shouldSetUnreadRuler) { // If a text display follows, use the minimum bubble margin after the // ruler, taking account of the absence of a shadow on the ruler. rulerMarginBottom = shadow - 1; } } } if (shouldSetUnreadRuler || shouldSetSessionRuler) { prev.style.marginBottom = rulerMarginBottom + "px"; prev.style.marginTop = kRulerMarginTop + "px"; } } else if (target.tagName == "P" && target.className == "event") { let parent = target.parentNode; // We need to start a group with this element if there are at least 4 // system messages and they aren't already grouped. if (!parent?.grouped && parent?.querySelector("p.event:nth-of-type(4)")) { let p = document.createElement("p"); p.className = "eventToggle"; p.addEventListener("click", event => event.target.parentNode.classList.toggle("hide-children") ); parent.insertBefore(p, parent.querySelector("p.event:nth-of-type(2)")); parent.classList.add("hide-children"); parent.grouped = true; } } if (msgTime) { updateLastMsgTime(msgTime); } } new MutationObserver(function (aMutations) { for (let mutation of aMutations) { for (let node of mutation.addedNodes) { if (node instanceof HTMLElement) { checkNewText(node); } } } }).observe(document.getElementById("ibcontent"), { childList: true, subtree: true, }); document.addEventListener("visibilitychange", visibilityChanged);