Commit b82bbca8 authored by Hugo Lefeuvre's avatar Hugo Lefeuvre

chatview: implement lazy display

Messages are now displayed block by block. When the user reaches the
top of the scrollbar, a new batch of messages gets loaded.

This allows better performances on large conversations.

Change-Id: Idc44df7149db6329982b2aa3420de6c014ce0924
Gitlab: #811Reviewed-by: Sébastien Blin's avatarSebastien Blin <sebastien.blin@savoirfairelinux.com>
parent 0babe2b4
......@@ -92,10 +92,21 @@ const avatar_size = 35
// without disabling the automatic go-back-to-bottom when a new message is
// received
const scrollDetectionThresh = 200
// printHistoryPart loads blocks of messages. Each block contains
// scrollBuffer messages
const scrollBuffer = 20
// The first time a conversation is loaded, the lazy loading system makes
// sure at least initialScrollBufferFactor screens of messages are loaded
const initialScrollBufferFactor = 3
// Some signal like the onscrolled signals are debounced so that the their
// assigned function isn't fired too often
const debounceTime = 200
/* Buffers */
var historyBufferIndex = 0 // When showing a large amount of interactions, this counter store the interaction's index to show
var historyBuffer = [] // Before showing a large amount of interactions, this array is used as a buffer.
// current index in the history buffer
var historyBufferIndex = 0
// buffer containing the conversation's messages
var historyBuffer = []
/* We retrieve refs to the most used navbar and message bar elements for efficiency purposes */
/* NOTE: always use getElementById when possible, way more efficient */
......@@ -118,6 +129,29 @@ var hasInvitation = false
var isTemporary = false
var isBanned = false
var isInitialLoading = false
var imagesLoadingCounter = 0
function onScrolled_() {
const messages = document.getElementById("messages")
if (messages.scrollTop == 0 && historyBufferIndex != historyBuffer.length) {
/* At the top and there's something to print */
printHistoryPart(messages.cloneNode(true), true, messages.scrollHeight)
}
}
const debounce = (fn, time) => {
let timeout
return function() {
const functionCall = () => fn.apply(this, arguments)
clearTimeout(timeout)
timeout = setTimeout(functionCall, time)
}
}
/* exported onScrolled */
var onScrolled = debounce(onScrolled_, debounceTime)
/**
* Generic wrapper. Execute passed function keeping scroll position identical.
......@@ -175,6 +209,9 @@ function back_to_bottom() {
*/
/* exported update_chatview_frame */
function update_chatview_frame(banned, temporary, alias, bestid) {
/* This function updates lots of things in the navbar and we don't want to
trigger that many DOM updates. Instead set display to none so DOM is
updated only once. */
navbar.style.display = "none"
hoverBackButtonAllowed = true
......@@ -326,6 +363,17 @@ function disableSendMessage(isDisabled)
messageBarInput.disabled = isDisabled
}
/* exported clearSenderImages */
function clearSenderImages()
{
var styles = document.head.querySelectorAll("style"),
i = styles.length
while (i--){
document.head.removeChild(styles[i])
}
}
/**
* This event handler adds the hover property back to the "back to welcome view"
* button.
......@@ -785,7 +833,10 @@ function updateFileInteraction(message_div, message_object, forceTypeToFile = fa
// Update flat buttons
var left_buttons = message_div.querySelector(".left_buttons")
left_buttons.innerHTML = ""
if (message_delivery_status === "awaiting peer" || message_delivery_status === "awaiting host" || message_delivery_status.indexOf("ongoing") === 0) {
if (message_delivery_status === "awaiting peer" ||
message_delivery_status === "awaiting host" ||
message_delivery_status.indexOf("ongoing") === 0) {
if (message_direction === "in" && message_delivery_status.indexOf("ongoing") !== 0) {
// add buttons to accept or refuse a call.
var accept_button = document.createElement("div")
......@@ -797,6 +848,7 @@ function updateFileInteraction(message_div, message_object, forceTypeToFile = fa
}
left_buttons.appendChild(accept_button)
}
var refuse_button = document.createElement("div")
refuse_button.innerHTML = refuseSvg
refuse_button.setAttribute("title", "Refuse")
......@@ -875,6 +927,7 @@ function mediaInteraction(message_id, link, ytid, noerror) {
linkElt.href = link
linkElt.style = "text-decoration: none; border:none;"
const imageElt = document.createElement("img")
/* Note, here, we don't check the size of the image.
in the future, we can check the content-type and content-length with a request
and maybe disable svg */
......@@ -882,15 +935,29 @@ function mediaInteraction(message_id, link, ytid, noerror) {
imageElt.setAttribute("onerror", "this.style.display='none'")
imageElt.src = ytid ? `http://img.youtube.com/vi/${ytid}/0.jpg` : link
if (isInitialLoading || messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh) {
/* Hack to keep the scrollbar at the bottom.
Images are loaded asynchronously and the scrollbar position is
changed each time an image is loaded and displayed. In order to
make sure the scrollbar stays at the bottom, reset scrollbar
const messages = document.getElementById("messages")
if (isInitialLoading) {
/* During initial load, make sure the scrollbar stays at the bottom.
Also, the final scrollHeight is only known after the last image was
loaded. We want to display a specific number of messages screens so
we have to set up a callback (on_image_load_finished) which will
check on that and reschedule a new display batch if not enough
messages have been loaded in the DOM. */
imageElt.onload = function() {
back_to_bottom()
on_image_load_finished()
}
} else if (messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh) {
/* Keep the scrollbar at the bottom. Images are loaded asynchronously and
the scrollbar position is changed each time an image is loaded and displayed.
In order to make sure the scrollbar stays at the bottom, reset scrollbar
position each time an image was loaded. */
imageElt.onload = back_to_bottom
}
linkElt.appendChild(imageElt)
imagesLoadingCounter++
if (ytid) {
media_wrapper.appendChild(buildVideoContainer(linkElt))
......@@ -971,8 +1038,8 @@ function actionInteraction() {
var message_wrapper = document.createElement("div")
message_wrapper.setAttribute("class", "message_wrapper")
// An file interaction contains buttons at the left of the interaction
// for the status or accept/refuse
// A file interaction contains buttons at the left of the interaction
// for the status or accept/refuse buttons
var left_buttons = document.createElement("div")
left_buttons.setAttribute("class", "left_buttons")
message_wrapper.appendChild(left_buttons)
......@@ -1300,33 +1367,85 @@ function updateMessage(message_object)
}
/**
* Display history in reverse order
* This function operates on passed root div.
* Make sure at least initialScrollBufferFactor screens of messages are
* available in the DOM.
*/
function on_image_load_finished() {
imagesLoadingCounter--
if (!imagesLoadingCounter) {
/* All images have been loaded and scrollHeight has now its final value.
Make sure enough messages have been displayed. */
const messages = document.getElementById("messages")
if (messages.scrollHeight < initialScrollBufferFactor * messages.clientHeight
&& historyBufferIndex !== historyBuffer.length) {
/* Not enough messages loaded, print a new batch. Enable isInitialLoading
as reloading a single batch might not be sufficient to fulfill our
criteria (we want to be called back again to check on that) */
isInitialLoading = true
printHistoryPart(messages.cloneNode(true), true, 0)
isInitialLoading = false
}
}
}
/**
* Display 'scrollBuffer' messages from history in passed div (reverse order).
*
* @param messages_div root div to modify
* @param setMessages if true and #messages doesn't exist, #messages is set to the resulting messages div
* @param messages_div that should be modified
* @param setMessages if enabled, #messages will be set to the resulting messages
* div after being modified. If #messages already exists it will
* be removed and replaced by the new div.
* @param fixedAt if setMessages is enabled, maintain scrollbar at the specified
* position (otherwise modifying #messages would result in
* changing the position of the scrollbar)
*/
function printHistoryPart(messages_div, setMessages = false)
function printHistoryPart(messages_div, setMessages = false, fixedAt)
{
isInitialLoading = true
if (historyBufferIndex === historyBuffer.length) {
return
}
for (; historyBufferIndex < historyBuffer.length; ++historyBufferIndex) {
for (var i = 0; i < scrollBuffer && historyBufferIndex < historyBuffer.length; ++historyBufferIndex && ++i) {
// TODO on-screen messages should be removed from the buffer
addOrUpdateMessage(historyBuffer[historyBuffer.length - 1 - historyBufferIndex], true, false, messages_div)
}
messages_div.lastChild.classList.add("last-message")
const messages = document.getElementById("messages")
if (!messages && setMessages) {
container.prepend(messages_div)
}
if (setMessages) {
const messages = document.getElementById("messages")
if (!document.getElementById("messages")) {
container.prepend(messages_div)
} else {
messages.parentNode.removeChild(messages)
container.prepend(messages_div)
}
isInitialLoading = false
if (fixedAt !== undefined) {
/* update scrollbar position to take text-message -related
scrollHeight changes in account (not necessary to wait
for DOM redisplay in this case). Changes due to image
messages are handled in their onLoad callbacks. */
back_to_scroll(fixedAt)
/* schedule a scrollbar position update for changes which
are neither handled by the previous call nor by onLoad
callbacks. This call is necessary but not sufficient,
dropping the previous call would result in visual
glitches during initial load. */
setTimeout(function() {back_to_scroll(fixedAt)}, 0)
}
}
}
/**
* Show the whole history in the chatview.
* @param messages_array array containing history to be printed
* Set history buffer, initialize messages div and display a first batch
* of messages.
*
* Make sure that enough messages are displayed to fill initialScrollBufferFactor
* screens of messages (if enough messages are present in the conversation)
*
* @param messages_array should contain history to be printed
*/
/* exported printHistory */
function printHistory(messages_array)
......@@ -1336,13 +1455,15 @@ function printHistory(messages_array)
var messages_div = document.createElement("div")
messages_div.setAttribute("id", "messages")
messages_div.setAttribute("onscroll", "onScrolled()")
printHistoryPart(messages_div, true)
setTimeout(back_to_bottom, 0)
isInitialLoading = true
printHistoryPart(messages_div, true, 0)
isInitialLoading = false
}
/**
* Sets the image for a given sender
* Set the image for a given sender
* set_sender_image object should contain the following keys:
* - sender: the name of the sender
* - sender_image: base64 png encoding of the sender image
......@@ -1371,16 +1492,5 @@ function setSenderImage(set_sender_image_object)
document.head.appendChild(style)
}
/* exported clearSenderImages */
function clearSenderImages()
{
var styles = document.head.querySelectorAll("style"),
i = styles.length
while (i--){
document.head.removeChild(styles[i])
}
}
</script>
</html>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment