chatview: fix scrollbar issues

Add a new wrapper function handling scrolling policy and use it
everywhere instead of doing code duplication. Remove dead code and
add meaningful comments.

Change-Id: I888377befdd939f3346629fb8c8df528c44140d2
Reviewed-by: Sebastien Blin <sebastien.blin@savoirfairelinux.com>
diff --git a/web/chatview.html b/web/chatview.html
index cd88dbf..fec9979 100644
--- a/web/chatview.html
+++ b/web/chatview.html
@@ -88,11 +88,14 @@
 /* Constants used at several places*/
 const messageBarPlaceHolder = "Type a message"
 const avatar_size = 35
-var raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame
-var displayLinksEnabled = false
+// scrollDetectionThresh represents the number of pixels a user can scroll
+// without disabling the automatic go-back-to-bottom when a new message is
+// received
+const scrollDetectionThresh = 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.
-var hoverBackButtonAllowed = true
 
 /* We retrieve refs to the most used navbar and message bar elements for efficiency purposes */
 /* NOTE: always use getElementById when possible, way more efficient */
@@ -109,9 +112,25 @@
 const invitationText    = document.getElementById("text")
 
 /* States: allows us to avoid re-doing something if it isn't meaningful */
-var isBanned = false
-var isTemporary = false
+var displayLinksEnabled = false
+var hoverBackButtonAllowed = true
 var hasInvitation = false
+var isTemporary = false
+var isBanned = false
+
+/**
+ * Generic wrapper. Execute passed function keeping scroll position identical.
+ *
+ * @param func function to execute
+ * @param args parameters as array
+ */
+function exec_keeping_scroll_position(func, args) {
+    var atEnd = messages.scrollTop >= messages.scrollHeight - messages.clientHeight - scrollDetectionThresh
+    func(...args)
+    if (atEnd) {
+        messages.scrollTop = messages.scrollHeight
+    }
+}
 
 /**
  * Update common frame between conversations.
@@ -234,21 +253,17 @@
  */
 /* exported grow_text_area */
 function grow_text_area() {
-    var is_at_bottom = messages.scrollTop === (messages.scrollHeight - messages.offsetHeight)
+    exec_keeping_scroll_position(function(){
+        var old_height = window.getComputedStyle(messageBar).height
+        messageBarInput.style.height = "auto" /* <-- necessary, no clue why */
+        messageBarInput.style.height = messageBarInput.scrollHeight + "px"
+        var new_height = window.getComputedStyle(messageBar).height
 
-    var old_height = window.getComputedStyle(messageBar).height
-    messageBarInput.style.height = "auto" /* <-- necessary, no clue why */
-    messageBarInput.style.height = messageBarInput.scrollHeight + "px"
-    var new_height = window.getComputedStyle(messageBar).height
+        var msgbar_size = window.getComputedStyle(document.body).getPropertyValue("--messagebar-size")
+        var total_size = parseInt(msgbar_size) + parseInt(new_height) - parseInt(old_height)
 
-    var msgbar_size = window.getComputedStyle(document.body).getPropertyValue("--messagebar-size")
-    var total_size = parseInt(msgbar_size) + parseInt(new_height) - parseInt(old_height)
-
-    document.body.style.setProperty("--messagebar-size", total_size.toString() + "px")
-
-    if (is_at_bottom) {
-        messages.scrollTop = messages.scrollHeight
-    }
+        document.body.style.setProperty("--messagebar-size", total_size.toString() + "px")
+    }, [])
 }
 
 /**
@@ -521,54 +536,58 @@
  */
 function cleanPreviousTimestamps (baseNode, endIndex) {
     // Remove previous elements with the same formatted timestamp
-    for (var c = endIndex - 1 ; c >= 0 ; --c) {
-        const child = messages.children[c]
-        if (child.className.indexOf("timestamp") !== -1) {
-            if (child.className === baseNode.className &&
+    exec_keeping_scroll_position(function() {
+        for (var c = endIndex - 1 ; c >= 0 ; --c) {
+            const child = messages.children[c]
+            if (child.className.indexOf("timestamp") !== -1) {
+                if (child.className === baseNode.className &&
             child.innerHTML === baseNode.innerHTML) {
-                messages.removeChild(child)
-            } else {
-                break // A different timestamp is met, we can stop here.
+                    messages.removeChild(child)
+                } else {
+                    break // A different timestamp is met, we can stop here.
+                }
             }
         }
-    }
+    }, [])
 }
 
 /**
  * Update timestamps.
  */
 function updateTimestamps() {
-    const timestamps = messages.getElementsByClassName(".timestamp")
-    const is_image_out = messages.querySelector(".message_out .sender_image")
-    const is_image_in = messages.querySelector(".message_in .sender_image")
-    if (timestamps) {
-        for ( var c = 0 ; c < messages.children.length ; ++c) {
-            const child = messages.children[c]
-            if (child.className.indexOf("timestamp") !== -1) {
+    exec_keeping_scroll_position(function() {
+        const timestamps = messages.getElementsByClassName(".timestamp")
+        const is_image_out = messages.querySelector(".message_out .sender_image")
+        const is_image_in = messages.querySelector(".message_in .sender_image")
+        if (timestamps) {
+            for ( var c = 0 ; c < messages.children.length ; ++c) {
+                const child = messages.children[c]
+                if (child.className.indexOf("timestamp") !== -1) {
                 // Update timestamp
-                child.innerHTML = getMessageTimestampText(child.getAttribute("message_timestamp"), true)
+                    child.innerHTML = getMessageTimestampText(child.getAttribute("message_timestamp"), true)
 
-                var desktop_margin = "25%"
-                const height = document.body.clientHeight
-                const width = document.body.clientWidth
-                if (width <= 1920 || height <= 1080) {
-                    desktop_margin = "15%"
+                    var desktop_margin = "25%"
+                    const height = document.body.clientHeight
+                    const width = document.body.clientWidth
+                    if (width <= 1920 || height <= 1080) {
+                        desktop_margin = "15%"
+                    }
+                    if (width <= 1000 || height <= 480) {
+                        desktop_margin = "0px"
+                    }
+                    if (child.className.indexOf("timestamp_out") !== -1) {
+                        const avatar_px = is_image_out ? (is_image_out.offsetHeight === avatar_size ? "60px" : "20px") : "20px"
+                        child.style.paddingRight = `calc(${desktop_margin} + ${avatar_px})`
+                    } else if (child.className.indexOf("timestamp_in") !== -1) {
+                        const avatar_px = is_image_in ? (is_image_in.offsetHeight === avatar_size ? "60px" : "20px") : "20px"
+                        child.style.paddingLeft = `calc(${desktop_margin} + ${avatar_px})`
+                    }
+                    // Remove previous elements with the same formatted timestamp
+                    cleanPreviousTimestamps(child, c)
                 }
-                if (width <= 1000 || height <= 480) {
-                    desktop_margin = "0px"
-                }
-                if (child.className.indexOf("timestamp_out") !== -1) {
-                    const avatar_px = is_image_out ? (is_image_out.offsetHeight === avatar_size ? "60px" : "20px") : "20px"
-                    child.style.paddingRight = `calc(${desktop_margin} + ${avatar_px})`
-                } else if (child.className.indexOf("timestamp_in") !== -1) {
-                    const avatar_px = is_image_in ? (is_image_in.offsetHeight === avatar_size ? "60px" : "20px") : "20px"
-                    child.style.paddingLeft = `calc(${desktop_margin} + ${avatar_px})`
-                }
-                // Remove previous elements with the same formatted timestamp
-                cleanPreviousTimestamps(child, c)
             }
         }
-    }
+    }, [])
 }
 
 /**
@@ -595,15 +614,17 @@
  * @param message_object
  */
 function updateProgressBar(progress_bar, message_object) {
-    var delivery_status = message_object["delivery_status"]
-    if ("progress" in message_object && !isErrorStatus(delivery_status) && message_object["progress"] !== 100) {
-        var progress_percent = (100 * message_object["progress"] / message_object["totalSize"])
-        if (progress_percent !== 100)
-            progress_bar.childNodes[0].setAttribute("style", "width: " + progress_percent + "%")
-        else
+    exec_keeping_scroll_position(function() {
+        var delivery_status = message_object["delivery_status"]
+        if ("progress" in message_object && !isErrorStatus(delivery_status) && message_object["progress"] !== 100) {
+            var progress_percent = (100 * message_object["progress"] / message_object["totalSize"])
+            if (progress_percent !== 100)
+                progress_bar.childNodes[0].setAttribute("style", "width: " + progress_percent + "%")
+            else
+                progress_bar.setAttribute("style", "display: none")
+        } else
             progress_bar.setAttribute("style", "display: none")
-    } else
-        progress_bar.setAttribute("style", "display: none")
+    }, [])
 }
 
 /**
@@ -684,22 +705,24 @@
 
     if (isImage(message_text) && message_delivery_status === "finished" && displayLinksEnabled && !forceTypeToFile) {
         // Replace the old wrapper by the downloaded image
-        if (message_div.querySelector(".message_wrapper")) {
-            var wrapper = message_div.querySelector(".message_wrapper")
-            wrapper.parentNode.removeChild(wrapper)
-        }
-        message_div.append(mediaInteraction(message_id, message_text))
-        message_div.querySelector("img").id = message_id
-        message_div.querySelector("img").msg_obj = message_object
-        message_div.querySelector("img").onerror = function() {
-            message_div = document.getElementById("#message_" + this.id)
-            var wrapper = message_div.querySelector(".message_wrapper")
-            if (wrapper) {
+        exec_keeping_scroll_position(function() {
+            if (message_div.querySelector(".message_wrapper")) {
+                var wrapper = message_div.querySelector(".message_wrapper")
                 wrapper.parentNode.removeChild(wrapper)
             }
-            message_div.append(fileInteraction(message_id))
-            updateFileInteraction(message_div, this.msg_obj, true)
-        }
+            message_div.append(mediaInteraction(message_id, message_text))
+            message_div.querySelector("img").id = message_id
+            message_div.querySelector("img").msg_obj = message_object
+            message_div.querySelector("img").onerror = function() {
+                message_div = document.getElementById("#message_" + this.id)
+                var wrapper = message_div.querySelector(".message_wrapper")
+                if (wrapper) {
+                    wrapper.parentNode.removeChild(wrapper)
+                }
+                message_div.append(fileInteraction(message_id))
+                updateFileInteraction(message_div, this.msg_obj, true)
+            }
+        }, [])
         return
     }
 
@@ -782,7 +805,7 @@
  * @param message_id
  * @param link to show
  * @param ytid if it's a youtube video
- * @param
+ * @param noerror
  */
 function mediaInteraction(message_id, link, ytid, noerror) {
     /* TODO promise?
@@ -1051,7 +1074,7 @@
         date_elt.setAttribute("class", timestamp_div_classes.join(" "))
         date_elt.setAttribute("message_timestamp", message_timestamp)
 
-        // Remove last timestamp if it's the same<h6></h6>
+        // Remove last timestamp if it's the same
         if (messages.getElementsByClassName(".timestamp"))
             cleanPreviousTimestamps(date_elt, messages.children.length)
 
@@ -1091,64 +1114,50 @@
 }
 
 /**
- * Add a message to the buffer.
+ * Wrapper for addOrUpdateMessage.
+ *
+ * This function adds or updates a message and makes sure the scrollbar position
+ * is refreshed correctly.
  *
  * @param message_object message to be added
  */
 /* exported addMessage */
 function addMessage(message_object)
 {
-    var atEnd = messages.scrollTop >= messages.scrollHeight - messages.clientHeight - 5
-    addOrUpdateMessage(message_object, true)
-    if (atEnd) {
-        var startTime = Date.now(),
-            durTime = 250.,
-            scrollStartHeight = messages.scrollHeight,
-            scrollStart = messages.scrollTop,
-            scrollDiff = scrollStartHeight - messages.clientHeight - scrollStart
-        function loop() {
-            var time = Date.now() - startTime,
-                scrollHeight = messages.scrollHeight,
-                diff = messages.scrollTop - scrollStart // If user scrolls up (diff > 0).
-
-            if (time >= durTime || scrollHeight != scrollStartHeight || diff < 0) {
-                if (diff >= 0) { // User scrolled up, don't autoscroll.
-                    messages.scrollTop = scrollHeight
-                }
-                return false
-            } else {
-                messages.scrollTop = scrollStart + (scrollDiff * (time/durTime))
-                raf(loop)
-            }
-        }
-
-
-        raf(loop) // Start the animation loop
-    }
+    exec_keeping_scroll_position(addOrUpdateMessage, [message_object, true])
 }
 
 /**
- * Show the history in reverse order
+ * Update a message that was previously added with addMessage
+ * @param message_object message to be updated
  */
-function printHistoryPart() {
+/* exported updateMessage */
+function updateMessage(message_object)
+{
+    exec_keeping_scroll_position(addOrUpdateMessage, [message_object, false])
+}
+
+/**
+ * This function displays the history in reverse order and makes sure the
+ * scrollbar position is refreshed correctly.
+ */
+function printHistoryPart()
+{
     if(historyBuffer.length == 0 || historyBufferIndex === historyBuffer.length) {
         // nothing to print
         return
     }
 
-    var previousScrollHeightMinusTop = messages.scrollHeight - messages.scrollTop
+    exec_keeping_scroll_position(function() {
+        // Show 10 messages
+        for (var i = 0; i < 10; ++i) {
+            addOrUpdateMessage(historyBuffer[historyBuffer.length - 1 - historyBufferIndex], true, false)
+            historyBufferIndex ++
 
-    // Show 10 messages
-    for (var i = 0; i < 10; ++i) {
-        addOrUpdateMessage(historyBuffer[historyBuffer.length - 1 - historyBufferIndex], true, false)
-        historyBufferIndex ++
-
-        if (historyBufferIndex === historyBuffer.length)
-            break
-    }
-
-    // Replace the scrollbar to the wanted position
-    messages.scrollTop = messages.scrollHeight - previousScrollHeightMinusTop
+            if (historyBufferIndex === historyBuffer.length)
+                break
+        }
+    }, [])
 
     // Make a short pause to minimize ressources consumption
     // show quickly the first hundred messages
@@ -1169,16 +1178,6 @@
 }
 
 /**
- * Update a message that was previously added with addMessage
- * @param message_object message to be updated
- */
-/* exported updateMessage */
-function updateMessage(message_object)
-{
-    addOrUpdateMessage(message_object, false)
-}
-
-/**
  * Sets the image for a given sender
  * set_sender_image object should contain the following keys:
  * - sender: the name of the sender