From 2e67dc1bd8bd64e313c77c03283c5b6cfccf8c77 Mon Sep 17 00:00:00 2001
From: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
Date: Tue, 6 Jul 2021 10:20:46 -0400
Subject: [PATCH] chatview: replace web chat view with qml listview

Introduces a primitive QML ListView based chat view lacking
features present in the previous web chat view, that will be added
in subsequent commits(styling, preview/media/link/file-transfer
message type support, etc.).

Gitlab: #467
Change-Id: Iedc40f6172a6cdacc48cda6f4187053fbf226713
---
 CMakeLists.txt                                |  14 +-
 qml.qrc                                       |   6 +-
 resources/misc/previewInterop.js              |  54 +++
 src/commoncomponents/MessageDelegate.qml      | 191 +++++++++
 src/constant/JamiTheme.qml                    |  26 +-
 src/conversationlistmodelbase.cpp             |  12 +-
 src/currentconversation.cpp                   |   1 +
 src/currentconversation.h                     |   1 +
 src/mainapplication.cpp                       |  14 +-
 src/mainapplication.h                         |   6 +-
 src/mainview/MainView.qml                     |  42 +-
 src/mainview/components/ChatView.qml          | 147 +++++++
 ...geWebViewFooter.qml => ChatViewFooter.qml} |   6 +-
 src/mainview/components/MessageBar.qml        |  54 +--
 .../components/MessageBarTextArea.qml         |   2 +-
 src/mainview/components/MessageListView.qml   | 106 +++++
 src/mainview/components/MessageWebView.qml    | 287 -------------
 .../components/MessageWebViewHeader.qml       |   2 +-
 src/mainview/components/ReadOnlyFooter.qml    |   2 +-
 src/messagesadapter.cpp                       | 389 +++++-------------
 src/messagesadapter.h                         | 107 ++---
 src/previewengine.cpp                         |  94 +++++
 src/previewengine.h                           |  64 +++
 src/qmlregister.cpp                           |  18 +-
 src/qmlregister.h                             |   2 +
 src/utils.cpp                                 |   2 +-
 src/webchathelpers.cpp                        | 159 -------
 src/webchathelpers.h                          |  42 --
 tests/qml/main.cpp                            |   3 +
 tests/qml/resources.qrc                       |   2 +-
 ...bViewFooter.qml => tst_ChatViewFooter.qml} |   4 +-
 tests/qml/src/tst_FilesToSendContainer.qml    |   4 +-
 32 files changed, 929 insertions(+), 934 deletions(-)
 create mode 100644 resources/misc/previewInterop.js
 create mode 100644 src/commoncomponents/MessageDelegate.qml
 create mode 100644 src/mainview/components/ChatView.qml
 rename src/mainview/components/{MessageWebViewFooter.qml => ChatViewFooter.qml} (96%)
 create mode 100644 src/mainview/components/MessageListView.qml
 delete mode 100644 src/mainview/components/MessageWebView.qml
 create mode 100644 src/previewengine.cpp
 create mode 100644 src/previewengine.h
 delete mode 100644 src/webchathelpers.cpp
 delete mode 100644 src/webchathelpers.h
 rename tests/qml/src/{tst_MessageWebViewFooter.qml => tst_ChatViewFooter.qml} (97%)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index f1d575f74..fd4f38627 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -45,7 +45,6 @@ set(COMMON_SOURCES
     ${SRC_DIR}/networkmanager.cpp
     ${SRC_DIR}/runguard.cpp
     ${SRC_DIR}/updatemanager.cpp
-    ${SRC_DIR}/webchathelpers.cpp
     ${SRC_DIR}/main.cpp
     ${SRC_DIR}/smartlistmodel.cpp
     ${SRC_DIR}/utils.cpp
@@ -87,7 +86,8 @@ set(COMMON_SOURCES
     ${SRC_DIR}/avatarregistry.cpp
     ${SRC_DIR}/currentconversation.cpp
     ${SRC_DIR}/currentaccount.cpp
-    ${SRC_DIR}/videodevices.cpp)
+    ${SRC_DIR}/videodevices.cpp
+    ${SRC_DIR}/previewengine.cpp)
 
 set(COMMON_HEADERS
     ${SRC_DIR}/avatarimageprovider.h
@@ -99,7 +99,6 @@ set(COMMON_HEADERS
     ${SRC_DIR}/version.h
     ${SRC_DIR}/accountlistmodel.h
     ${SRC_DIR}/runguard.h
-    ${SRC_DIR}/webchathelpers.h
     ${SRC_DIR}/rendermanager.h
     ${SRC_DIR}/connectivitymonitor.h
     ${SRC_DIR}/jamiavatartheme.h
@@ -144,7 +143,8 @@ set(COMMON_HEADERS
     ${SRC_DIR}/avatarregistry.h
     ${SRC_DIR}/currentconversation.h
     ${SRC_DIR}/currentaccount.h
-    ${SRC_DIR}/videodevices.h)
+    ${SRC_DIR}/videodevices.h
+    ${SRC_DIR}/previewengine.h)
 
 set(QML_LIBS
     Qt5::Quick
@@ -155,7 +155,8 @@ set(QML_LIBS
     Qt5::Concurrent
     Qt5::QuickControls2
     Qt5::WebEngine
-    Qt5::Core)
+    Qt5::Core
+    Qt5::WebEngineWidgets)
 
 set(QML_LIBS_LIST
     Core
@@ -166,7 +167,8 @@ set(QML_LIBS_LIST
     Svg
     Sql
     QuickControls2
-    WebEngine)
+    WebEngine
+    WebEngineWidgets)
 
 set(WINDOWS_SYS_LIBS Shell32.lib
                      Ole32.lib
diff --git a/qml.qrc b/qml.qrc
index 4a6ff244c..91803d64b 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -94,7 +94,7 @@
         <file>src/mainview/components/AboutPopUp.qml</file>
         <file>src/mainview/components/SidePanel.qml</file>
         <file>src/mainview/components/WelcomePage.qml</file>
-        <file>src/mainview/components/MessageWebView.qml</file>
+        <file>src/mainview/components/ChatView.qml</file>
         <file>src/mainview/components/MessageWebViewHeader.qml</file>
         <file>src/mainview/components/AccountComboBox.qml</file>
         <file>src/mainview/components/CallStackView.qml</file>
@@ -143,7 +143,7 @@
         <file>src/commoncomponents/contextmenu/GeneralMenuSeparator.qml</file>
         <file>src/mainview/components/ParticipantOverlayButton.qml</file>
         <file>src/mainview/components/ParticipantControlLayout.qml</file>
-        <file>src/mainview/components/MessageWebViewFooter.qml</file>
+        <file>src/mainview/components/ChatViewFooter.qml</file>
         <file>src/commoncomponents/emojipicker/EmojiPicker.qml</file>
         <file>src/commoncomponents/emojipicker/emojiPickerLoader.js</file>
         <file>src/commoncomponents/emojipicker/emojiPickerLoader.html</file>
@@ -161,5 +161,7 @@
         <file>src/commoncomponents/BackButton.qml</file>
         <file>src/commoncomponents/JamiSwitch.qml</file>
         <file>src/mainview/components/ReadOnlyFooter.qml</file>
+        <file>src/commoncomponents/MessageDelegate.qml</file>
+        <file>src/mainview/components/MessageListView.qml</file>
     </qresource>
 </RCC>
diff --git a/resources/misc/previewInterop.js b/resources/misc/previewInterop.js
new file mode 100644
index 000000000..4a8a30291
--- /dev/null
+++ b/resources/misc/previewInterop.js
@@ -0,0 +1,54 @@
+_ = new QWebChannel(qt.webChannelTransport, function (channel) {
+    window.jsbridge = channel.objects.jsbridge
+})
+
+function log(msg) {
+    window.jsbridge.log(msg)
+}
+
+function getPreviewInfo(messageId, url) {
+    var title = null
+    var description = null
+    var image = null
+    if (!url.includes("http://") && !url.includes("https://")) {
+        url = "http://".concat(url)
+    }
+    fetch(url, {
+              mode: 'no-cors',
+              headers: {'Set-Cookie': 'SameSite=None; Secure'}
+          }).then(function (response) {
+        return response.text()
+    }).then(function (html) {
+        // create DOM from html string
+        var parser = new DOMParser()
+        var doc = parser.parseFromString(html, "text/html")
+        if (!url.includes("twitter.com")){
+            title = getTitle(doc)
+            image = getImage(doc, url)
+            description = getDescription(doc)
+            var domain = (new URL(url))
+            domain = (domain.hostname).replace("www.", "")
+        } else {
+            title = "Twitter. It's what's happening."
+        }
+
+        window.jsbridge.infoReady(messageId, {
+                                      'title': title,
+                                      'image': image,
+                                      'description': description,
+                                      'url': url,
+                                      'domain': domain,
+                                  })
+    }).catch(function (err) {
+        log("Error occured while fetching document: " + err)
+    })
+}
+
+function parseMessage(messageId, message) {
+    var links = linkify.find(message)
+    if (links.length === 0) {
+        return
+    }
+    getPreviewInfo(messageId, links[0].href)
+    window.jsbridge.linkifyReady(messageId, linkifyStr(message))
+}
diff --git a/src/commoncomponents/MessageDelegate.qml b/src/commoncomponents/MessageDelegate.qml
new file mode 100644
index 000000000..fca345efa
--- /dev/null
+++ b/src/commoncomponents/MessageDelegate.qml
@@ -0,0 +1,191 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+import QtWebEngine 1.10
+
+import net.jami.Models 1.1
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+
+Control {
+    id: root
+
+    readonly property bool isGenerated: Type === Interaction.Type.CALL ||
+                                        Type === Interaction.Type.CONTACT
+    readonly property string author: Author
+    readonly property var timestamp: Timestamp
+    readonly property bool isOutgoing: model.Author === ""
+    readonly property var formattedTime: MessagesAdapter.getFormattedTime(Timestamp)
+    readonly property bool isImage: MessagesAdapter.isImage(Body)
+    readonly property bool isAnimatedImage: MessagesAdapter.isAnimatedImage(Body)
+    readonly property var linkPreviewInfo: LinkPreviewInfo
+
+    readonly property var body: Body
+    readonly property real msgMargin: 64
+
+    width: parent ? parent.width : 0
+    height: loader.height
+
+    Loader {
+        id: loader
+
+        property alias isOutgoing: root.isOutgoing
+        property alias isGenerated: root.isGenerated
+        readonly property var author: Author
+        readonly property var body: Body
+
+        sourceComponent: isGenerated ?
+                             generatedMsgComp :
+                             userMsgComp
+    }
+
+    Component {
+        id: generatedMsgComp
+
+        Column {
+            width: root.width
+            spacing: 2
+
+            TextArea {
+                width: parent.width
+                text: body
+                horizontalAlignment: Qt.AlignHCenter
+                readOnly: true
+                font.pointSize: 11
+                color: JamiTheme.chatviewTextColor
+            }
+
+            Item {
+                id: infoCell
+
+                width: parent.width
+                height: childrenRect.height
+
+                Component.onCompleted: children = timestampLabel
+            }
+
+            bottomPadding: 12
+        }
+    }
+
+    Component {
+        id: userMsgComp
+
+        GridLayout {
+            id: gridLayout
+
+            width: root.width
+
+            columns: 2
+            rows: 2
+
+            columnSpacing: 2
+            rowSpacing: 2
+
+            Column {
+                id: msgCell
+
+                Layout.column: isOutgoing ? 0 : 1
+                Layout.row: 0
+                Layout.fillWidth: true
+                Layout.maximumWidth: 640
+                Layout.preferredHeight: childrenRect.height
+                Layout.alignment: isOutgoing ? Qt.AlignRight : Qt.AlignLeft
+                Layout.leftMargin: isOutgoing ? msgMargin : 0
+                Layout.rightMargin: isOutgoing ? 0 : msgMargin
+
+                Control {
+                    id: msgBlock
+
+                    width: parent.width
+
+                    contentItem: Column {
+                        id: msgContent
+
+                        property real txtWidth: ta.contentWidth + 3 * ta.padding
+
+                        TextArea {
+                            id: ta
+                            width: parent.width
+                            text: body
+                            padding: 10
+                            font.pointSize: 11
+                            font.hintingPreference: Font.PreferNoHinting
+                            renderType: Text.NativeRendering
+                            textFormat: TextEdit.RichText
+                            wrapMode: Text.WrapAtWordBoundaryOrAnywhere
+                            transform: Translate { x: bg.x }
+                            rightPadding: isOutgoing ? padding * 1.5 : 0
+                            color: isOutgoing ?
+                                       JamiTheme.messageOutTxtColor :
+                                       JamiTheme.messageInTxtColor
+                        }
+                    }
+                    background: Rectangle {
+                        id: bg
+
+                        anchors.right: isOutgoing ? msgContent.right : undefined
+                        width: msgContent.txtWidth
+                        radius: 18
+                        color: isOutgoing ?
+                                   JamiTheme.messageOutBgColor :
+                                   JamiTheme.messageInBgColor
+                    }
+                }
+            }
+            Item {
+                id: infoCell
+
+                Layout.column: isOutgoing ? 0 : 1
+                Layout.row: 1
+                Layout.fillWidth: true
+                Layout.preferredHeight: childrenRect.height
+
+                Component.onCompleted: children = timestampLabel
+            }
+            Item {
+                id: avatarCell
+
+                Layout.column: isOutgoing ? 1 : 0
+                Layout.row: 0
+                Layout.preferredWidth: isOutgoing ? 16 : avatar.width
+                Layout.preferredHeight: msgCell.height
+                Layout.leftMargin: isOutgoing ? 0 : 6
+                Layout.rightMargin: Layout.leftMargin
+                Avatar {
+                    id: avatar
+                    visible: !isOutgoing
+                    anchors.bottom: parent.bottom
+                    width: 32
+                    height: 32
+                    imageId: author
+                    showPresenceIndicator: false
+                    mode: Avatar.Mode.Contact
+                }
+            }
+        }
+    }
+
+    Label {
+        id: timestampLabel
+
+        text: formattedTime
+        color: JamiTheme.timestampColor
+
+        anchors.right: isGenerated || !isOutgoing ? undefined : parent.right
+        anchors.rightMargin: 6
+        anchors.left: isGenerated || isOutgoing ? undefined : parent.left
+        anchors.leftMargin: 6
+        anchors.horizontalCenter: isGenerated ? parent.horizontalCenter : undefined
+    }
+
+    opacity: 0
+    Behavior on opacity { NumberAnimation { duration: 40 } }
+
+    Component.onCompleted: {
+        opacity = 1
+        if (!Linkified && !isImage && !isAnimatedImage) {
+            MessagesAdapter.parseMessageUrls(Id, Body)
+        }
+    }
+}
diff --git a/src/constant/JamiTheme.qml b/src/constant/JamiTheme.qml
index ca3bc230f..4f0c93ef7 100644
--- a/src/constant/JamiTheme.qml
+++ b/src/constant/JamiTheme.qml
@@ -147,12 +147,12 @@ Item {
     // Chatview
     property color jamiLightBlue: darkTheme ? "#003b4e" : Qt.rgba(59, 193, 211, 0.3)
     property color jamiDarkBlue: darkTheme ? "#28b1ed" : "#003b4e"
-    property color chatviewTextColor: textColor
+    property color chatviewTextColor: darkTheme ? "#f0f0f0" : "#353637"
     property color timestampColor: darkTheme ? "#bbb" : "#333"
     property color messageOutBgColor: darkTheme ? "#28b1ed" : "#cfd8dc"
-    property color messageOutTxtColor: textColor
+    property color messageOutTxtColor: chatviewTextColor
     property color messageInBgColor: darkTheme? "#616161" : "#cfebf5"
-    property color messageInTxtColor: textColor
+    property color messageInTxtColor: chatviewTextColor
     property color fileOutTimestampColor: darkTheme ? "#eee" : "#555"
     property color fileInTimestampColor: darkTheme ? "#999" : "#555"
     property color chatviewBgColor: darkTheme ? bgDarkMode_ : whiteColor
@@ -271,17 +271,17 @@ Item {
     property real modalPopupDropShadowSamples: 16
 
     // MessageWebView
-    property real messageWebViewHairLineSize: 1
+    property real chatViewHairLineSize: 1
     property real messageWebViewHeaderPreferredHeight: 64
-    property real messageWebViewFooterContentMaximumWidth: 1000
-    property real messageWebViewFooterPreferredHeight: 50
-    property real messageWebViewFooterMaximumHeight: 280
-    property real messageWebViewFooterRowSpacing: 1
-    property real messageWebViewFooterButtonSize: 36
-    property real messageWebViewFooterButtonIconSize: 48
-    property real messageWebViewFooterButtonRadius: 5
-    property real messageWebViewFooterFileContainerPreferredHeight: 150
-    property real messageWebViewFooterTextAreaMaximumHeight: 130
+    property real chatViewMaximumWidth: 900
+    property real chatViewFooterPreferredHeight: 50
+    property real chatViewFooterMaximumHeight: 280
+    property real chatViewFooterRowSpacing: 1
+    property real chatViewFooterButtonSize: 36
+    property real chatViewFooterButtonIconSize: 48
+    property real chatViewFooterButtonRadius: 5
+    property real chatViewFooterFileContainerPreferredHeight: 150
+    property real chatViewFooterTextAreaMaximumHeight: 130
 
     // MessageWebView File Transfer Container
     property real filesToSendContainerSpacing: 5
diff --git a/src/conversationlistmodelbase.cpp b/src/conversationlistmodelbase.cpp
index 25eb8b821..98df36e13 100644
--- a/src/conversationlistmodelbase.cpp
+++ b/src/conversationlistmodelbase.cpp
@@ -108,22 +108,22 @@ ConversationListModelBase::dataForItem(item_t item, int role) const
     case Role::UnreadMessagesCount:
         return QVariant(item.unreadMessages);
     case Role::LastInteractionTimeStamp: {
-        if (!item.interactions.empty()) {
-            auto ts = static_cast<qint32>(item.interactions.at(item.lastMessageUid).timestamp);
+        if (!item.interactions->empty()) {
+            auto ts = static_cast<qint32>(item.interactions->at(item.lastMessageUid).timestamp);
             return QVariant(ts);
         }
         break;
     }
     case Role::LastInteractionDate: {
-        if (!item.interactions.empty()) {
+        if (!item.interactions->empty()) {
             return QVariant(
-                Utils::formatTimeString(item.interactions.at(item.lastMessageUid).timestamp));
+                Utils::formatTimeString(item.interactions->at(item.lastMessageUid).timestamp));
         }
         break;
     }
     case Role::LastInteraction: {
-        if (!item.interactions.empty()) {
-            return QVariant(item.interactions.at(item.lastMessageUid).body);
+        if (!item.interactions->empty()) {
+            return QVariant(item.interactions->at(item.lastMessageUid).body);
         }
         break;
     }
diff --git a/src/currentconversation.cpp b/src/currentconversation.cpp
index e0ee604ac..1f7f279cb 100644
--- a/src/currentconversation.cpp
+++ b/src/currentconversation.cpp
@@ -60,6 +60,7 @@ CurrentConversation::updateData()
             set_needsSyncing(convInfo.needsSyncing);
             set_isSip(accInfo.profileInfo.type == profile::Type::SIP);
             set_callId(convInfo.getCallId());
+            set_allMessagesLoaded(convInfo.allMessagesLoaded);
             if (accInfo.callModel->hasCall(callId_)) {
                 auto call = accInfo.callModel->getCall(callId_);
                 set_callState(call.status);
diff --git a/src/currentconversation.h b/src/currentconversation.h
index 1e0e14fae..e2464b8f6 100644
--- a/src/currentconversation.h
+++ b/src/currentconversation.h
@@ -44,6 +44,7 @@ class CurrentConversation final : public QObject
     QML_PROPERTY(bool, inCall)
     QML_PROPERTY(bool, isTemporary)
     QML_PROPERTY(bool, isContact)
+    QML_PROPERTY(bool, allMessagesLoaded)
 
 public:
     explicit CurrentConversation(LRCInstance* lrcInstance, QObject* parent = nullptr);
diff --git a/src/mainapplication.cpp b/src/mainapplication.cpp
index 70146ef4a..34cee61ca 100644
--- a/src/mainapplication.cpp
+++ b/src/mainapplication.cpp
@@ -1,4 +1,4 @@
-/*!
+/*
  * Copyright (C) 2015-2020 by Savoir-faire Linux
  * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@@ -25,6 +25,7 @@
 #include "appsettingsmanager.h"
 #include "connectivitymonitor.h"
 #include "systemtray.h"
+#include "previewengine.h"
 
 #include <QAction>
 #include <QCommandLineParser>
@@ -149,18 +150,12 @@ MainApplication::MainApplication(int& argc, char** argv)
     , connectivityMonitor_(new ConnectivityMonitor(this))
     , settingsManager_(new AppSettingsManager(this))
     , systemTray_(new SystemTray(settingsManager_.get(), this))
+    , previewEngine_(new PreviewEngine(this))
 {
     QObject::connect(this, &QApplication::aboutToQuit, [this] { cleanup(); });
 }
 
-MainApplication::~MainApplication()
-{
-    engine_.reset();
-    systemTray_.reset();
-    settingsManager_.reset();
-    lrcInstance_.reset();
-    connectivityMonitor_.reset();
-}
+MainApplication::~MainApplication() {}
 
 bool
 MainApplication::init()
@@ -414,6 +409,7 @@ MainApplication::initQmlLayer()
                          systemTray_.get(),
                          lrcInstance_.get(),
                          settingsManager_.get(),
+                         previewEngine_.get(),
                          &screenInfo_,
                          this);
 
diff --git a/src/mainapplication.h b/src/mainapplication.h
index 66c53d6ce..f7fbc82de 100644
--- a/src/mainapplication.h
+++ b/src/mainapplication.h
@@ -1,4 +1,4 @@
-/*!
+/*
  * Copyright (C) 2020 by Savoir-faire Linux
  * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
@@ -35,6 +35,7 @@ class ConnectivityMonitor;
 class AppSettingsManager;
 class SystemTray;
 class CallAdapter;
+class PreviewEngine;
 
 // Provides information about the screen the app is displayed on
 class ScreenInfo : public QObject
@@ -97,8 +98,7 @@ private:
     QScopedPointer<ConnectivityMonitor> connectivityMonitor_;
     QScopedPointer<AppSettingsManager> settingsManager_;
     QScopedPointer<SystemTray> systemTray_;
+    QScopedPointer<PreviewEngine> previewEngine_;
 
     ScreenInfo screenInfo_;
-
-    CallAdapter* callAdapter_;
 };
diff --git a/src/mainview/MainView.qml b/src/mainview/MainView.qml
index 97ffa3e2b..c3cb6f801 100644
--- a/src/mainview/MainView.qml
+++ b/src/mainview/MainView.qml
@@ -76,8 +76,8 @@ Rectangle {
         callStackView.needToCloseInCallConversationAndPotentialWindow()
         LRCInstance.deselectConversation()
         if (isPageInStack("callStackViewObject", sidePanelViewStack) ||
-                isPageInStack("communicationPageMessageWebView", sidePanelViewStack) ||
-                isPageInStack("communicationPageMessageWebView", mainViewStack) ||
+                isPageInStack("chatView", sidePanelViewStack) ||
+                isPageInStack("chatView", mainViewStack) ||
                 isPageInStack("callStackViewObject", mainViewStack)) {
             sidePanelViewStack.pop(StackView.Immediate)
             mainViewStack.pop(welcomePage, StackView.Immediate)
@@ -98,10 +98,10 @@ Rectangle {
     function pushCommunicationMessageWebView() {
         if (sidePanelOnly) {
             sidePanelViewStack.pop(StackView.Immediate)
-            sidePanelViewStack.push(communicationPageMessageWebView, StackView.Immediate)
+            sidePanelViewStack.push(chatView, StackView.Immediate)
         } else {
             mainViewStack.pop(welcomePage, StackView.Immediate)
-            mainViewStack.push(communicationPageMessageWebView, StackView.Immediate)
+            mainViewStack.push(chatView, StackView.Immediate)
         }
     }
 
@@ -164,24 +164,17 @@ Rectangle {
     }
 
     function setMainView(convId) {
-        if (!(communicationPageMessageWebView.jsLoaded)) {
-            communicationPageMessageWebView.jsLoadedChanged.connect(
-                        function(convId) {
-                            return function() { setMainView(convId) }
-                        }(convId))
-            return
-        }
         var item = ConversationsAdapter.getConvInfoMap(convId)
         if (item.convId === undefined)
             return
-        communicationPageMessageWebView.headerUserAliasLabelText = item.title
-        communicationPageMessageWebView.headerUserUserNameLabelText = item.bestId
+        chatView.headerUserAliasLabelText = item.title
+        chatView.headerUserUserNameLabelText = item.bestId
         if (item.callStackViewShouldShow) {
             if (inSettingsView) {
                 toggleSettingsView()
             }
             MessagesAdapter.setupChatView(item)
-            callStackView.setLinkedWebview(communicationPageMessageWebView)
+            callStackView.setLinkedWebview(chatView)
             callStackView.responsibleAccountId = LRCInstance.currentAccountId
             callStackView.responsibleConvUid = convId
             callStackView.isAudioOnly = item.isAudioOnly
@@ -201,13 +194,13 @@ Rectangle {
                 callStackView.needToCloseInCallConversationAndPotentialWindow()
                 MessagesAdapter.setupChatView(item)
                 pushCommunicationMessageWebView()
-                communicationPageMessageWebView.focusMessageWebView()
+                chatView.focusChatView()
                 currentConvUID = convId
             } else if (isPageInStack("callStackViewObject", sidePanelViewStack)
                        || isPageInStack("callStackViewObject", mainViewStack)) {
                 callStackView.needToCloseInCallConversationAndPotentialWindow()
                 pushCommunicationMessageWebView()
-                communicationPageMessageWebView.focusMessageWebView()
+                chatView.focusChatView()
             }
         }
     }
@@ -396,21 +389,12 @@ Rectangle {
         onSettingsBackArrowClicked: sidePanelViewStack.pop(StackView.Immediate)
     }
 
-    MessageWebView {
-        id: communicationPageMessageWebView
-
-        objectName: "communicationPageMessageWebView"
-
-        signal toSendMessageContentSaved(string arg)
-        signal toMessagesCleared
-        signal toMessagesLoaded
+    ChatView {
+        id: chatView
 
+        objectName: "chatView"
         visible: false
-
-        Component.onCompleted: {
-            // Set qml MessageWebView object pointer to c++.
-            MessagesAdapter.setQmlObject(this)
-        }
+        Component.onCompleted: MessagesAdapter.setQmlObject(this)
     }
 
     onWidthChanged: {
diff --git a/src/mainview/components/ChatView.qml b/src/mainview/components/ChatView.qml
new file mode 100644
index 000000000..1336f1435
--- /dev/null
+++ b/src/mainview/components/ChatView.qml
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2020-2021 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ * Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import net.jami.Models 1.1
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+
+import "../../commoncomponents"
+import "../js/pluginhandlerpickercreation.js" as PluginHandlerPickerCreation
+
+Rectangle {
+    id: root
+
+    property string headerUserAliasLabelText: ""
+    property string headerUserUserNameLabelText: ""
+
+    property bool allMessagesLoaded
+
+    signal needToHideConversationInCall
+    signal messagesCleared
+    signal messagesLoaded
+
+    function focusChatView() {
+        chatViewFooter.textInput.forceActiveFocus()
+    }
+
+    color: JamiTheme.chatviewBgColor
+
+    ColumnLayout {
+        anchors.fill: root
+
+        spacing: 0
+
+        MessageWebViewHeader {
+            id: messageWebViewHeader
+
+            Layout.alignment: Qt.AlignHCenter
+            Layout.fillWidth: true
+            Layout.preferredHeight: JamiTheme.messageWebViewHeaderPreferredHeight
+            Layout.maximumHeight: JamiTheme.messageWebViewHeaderPreferredHeight
+
+            userAliasLabelText: headerUserAliasLabelText
+            userUserNameLabelText: headerUserUserNameLabelText
+
+            DropArea {
+                anchors.fill: parent
+                onDropped: chatViewFooter.setFilePathsToSend(drop.urls)
+            }
+
+            onBackClicked: {
+                mainView.showWelcomeView()
+            }
+
+            onNeedToHideConversationInCall: {
+                root.needToHideConversationInCall()
+            }
+
+            onPluginSelector: {
+                // Create plugin handler picker - PLUGINS
+                PluginHandlerPickerCreation.createPluginHandlerPickerObjects(
+                            root, false)
+                PluginHandlerPickerCreation.calculateCurrentGeo(root.width / 2,
+                                                                root.height / 2)
+                PluginHandlerPickerCreation.openPluginHandlerPicker()
+            }
+        }
+
+        StackLayout {
+            id: chatViewStack
+
+            Layout.alignment: Qt.AlignHCenter
+            Layout.fillWidth: true
+            Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
+            Layout.fillHeight: true
+            Layout.topMargin: JamiTheme.chatViewHairLineSize
+            Layout.bottomMargin: JamiTheme.chatViewHairLineSize
+
+            currentIndex: CurrentConversation.isRequest ||
+                          CurrentConversation.needsSyncing
+
+            Loader {
+                active: CurrentConversation.id !== ""
+                sourceComponent: MessageListView {
+                    DropArea {
+                        anchors.fill: parent
+                        onDropped: chatViewFooter.setFilePathsToSend(drop.urls)
+                    }
+                }
+            }
+
+            InvitationView {
+                id: invitationView
+
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+            }
+        }
+
+        ReadOnlyFooter {
+            visible: CurrentConversation.readOnly
+            Layout.fillWidth: true
+        }
+
+        ChatViewFooter {
+            id: chatViewFooter
+
+            visible: {
+                if (CurrentConversation.needsSyncing || CurrentConversation.readOnly)
+                    return false
+                else if (CurrentConversation.isSwarm && CurrentConversation.isRequest)
+                    return false
+                return true
+            }
+
+            Layout.alignment: Qt.AlignHCenter
+            Layout.fillWidth: true
+            Layout.preferredHeight: implicitHeight
+            Layout.maximumHeight: JamiTheme.chatViewFooterMaximumHeight
+
+            DropArea {
+                anchors.fill: parent
+                onDropped: chatViewFooter.setFilePathsToSend(drop.urls)
+            }
+        }
+    }
+}
diff --git a/src/mainview/components/MessageWebViewFooter.qml b/src/mainview/components/ChatViewFooter.qml
similarity index 96%
rename from src/mainview/components/MessageWebViewFooter.qml
rename to src/mainview/components/ChatViewFooter.qml
index bc5ca7c18..b0f58069d 100644
--- a/src/mainview/components/MessageWebViewFooter.qml
+++ b/src/mainview/components/ChatViewFooter.qml
@@ -136,7 +136,7 @@ Rectangle {
                 emojiPicker.y = Qt.binding(function() {
                     var buttonY = JamiQmlUtils.audioRecordMessageButtonInMainViewPoint.y
                     return buttonY - emojiPicker.height - messageBar.marginSize
-                            - JamiTheme.messageWebViewHairLineSize
+                            - JamiTheme.chatViewHairLineSize
                 })
 
                 emojiPicker.openEmojiPicker()
@@ -201,9 +201,9 @@ Rectangle {
 
             Layout.alignment: Qt.AlignHCenter
             Layout.preferredWidth: footerColumnLayout.width
-            Layout.maximumWidth: JamiTheme.messageWebViewFooterContentMaximumWidth
+            Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
             Layout.preferredHeight: filesToSendCount ?
-                                        JamiTheme.messageWebViewFooterFileContainerPreferredHeight : 0
+                                        JamiTheme.chatViewFooterFileContainerPreferredHeight : 0
         }
     }
 }
diff --git a/src/mainview/components/MessageBar.qml b/src/mainview/components/MessageBar.qml
index 4b0f31883..b9a8c8628 100644
--- a/src/mainview/components/MessageBar.qml
+++ b/src/mainview/components/MessageBar.qml
@@ -48,9 +48,9 @@ ColumnLayout {
         id: messageBarHairLine
 
         Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
-        Layout.preferredHeight: JamiTheme.messageWebViewHairLineSize
+        Layout.preferredHeight: JamiTheme.chatViewHairLineSize
         Layout.fillWidth: true
-        Layout.maximumWidth: JamiTheme.messageWebViewFooterContentMaximumWidth
+        Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
 
         color: JamiTheme.tabbarBorderColor
     }
@@ -60,20 +60,20 @@ ColumnLayout {
 
         Layout.alignment: Qt.AlignCenter
         Layout.fillWidth: true
-        Layout.maximumWidth: JamiTheme.messageWebViewFooterContentMaximumWidth
+        Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
 
-        spacing: JamiTheme.messageWebViewFooterRowSpacing
+        spacing: JamiTheme.chatViewFooterRowSpacing
 
         PushButton {
             id: sendFileButton
 
             Layout.alignment: Qt.AlignVCenter
             Layout.leftMargin: marginSize
-            Layout.preferredWidth: JamiTheme.messageWebViewFooterButtonSize
-            Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
+            Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
+            Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
 
-            radius: JamiTheme.messageWebViewFooterButtonRadius
-            preferredSize: JamiTheme.messageWebViewFooterButtonIconSize - 6
+            radius: JamiTheme.chatViewFooterButtonRadius
+            preferredSize: JamiTheme.chatViewFooterButtonIconSize - 6
 
             toolTipText: JamiStrings.sendFile
 
@@ -89,11 +89,11 @@ ColumnLayout {
             id: audioRecordMessageButton
 
             Layout.alignment: Qt.AlignVCenter
-            Layout.preferredWidth: JamiTheme.messageWebViewFooterButtonSize
-            Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
+            Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
+            Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
 
-            radius: JamiTheme.messageWebViewFooterButtonRadius
-            preferredSize: JamiTheme.messageWebViewFooterButtonIconSize
+            radius: JamiTheme.chatViewFooterButtonRadius
+            preferredSize: JamiTheme.chatViewFooterButtonIconSize
 
             toolTipText: JamiStrings.leaveAudioMessage
 
@@ -111,11 +111,11 @@ ColumnLayout {
             id: videoRecordMessageButton
 
             Layout.alignment: Qt.AlignVCenter
-            Layout.preferredWidth: JamiTheme.messageWebViewFooterButtonSize
-            Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
+            Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
+            Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
 
-            radius: JamiTheme.messageWebViewFooterButtonRadius
-            preferredSize: JamiTheme.messageWebViewFooterButtonIconSize
+            radius: JamiTheme.chatViewFooterButtonRadius
+            preferredSize: JamiTheme.chatViewFooterButtonIconSize
 
             toolTipText: JamiStrings.leaveVideoMessage
 
@@ -144,10 +144,10 @@ ColumnLayout {
             Layout.fillWidth: true
             Layout.margins: marginSize / 2
             Layout.preferredHeight: {
-                return JamiTheme.messageWebViewFooterPreferredHeight
-                        > contentHeight ? JamiTheme.messageWebViewFooterPreferredHeight : contentHeight
+                return JamiTheme.chatViewFooterPreferredHeight
+                        > contentHeight ? JamiTheme.chatViewFooterPreferredHeight : contentHeight
             }
-            Layout.maximumHeight: JamiTheme.messageWebViewFooterTextAreaMaximumHeight
+            Layout.maximumHeight: JamiTheme.chatViewFooterTextAreaMaximumHeight
                                   - marginSize / 2
 
             onSendMessagesRequired: root.sendMessageButtonClicked()
@@ -158,11 +158,11 @@ ColumnLayout {
 
             Layout.alignment: Qt.AlignVCenter
             Layout.rightMargin: sendMessageButton.visible ? 0 : marginSize
-            Layout.preferredWidth: JamiTheme.messageWebViewFooterButtonSize
-            Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
+            Layout.preferredWidth: JamiTheme.chatViewFooterButtonSize
+            Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
 
-            radius: JamiTheme.messageWebViewFooterButtonRadius
-            preferredSize: JamiTheme.messageWebViewFooterButtonIconSize
+            radius: JamiTheme.chatViewFooterButtonRadius
+            preferredSize: JamiTheme.chatViewFooterButtonIconSize
 
             toolTipText: JamiStrings.addEmoji
 
@@ -183,11 +183,11 @@ ColumnLayout {
 
             Layout.alignment: Qt.AlignVCenter
             Layout.rightMargin: visible ? marginSize : 0
-            Layout.preferredWidth: scale * JamiTheme.messageWebViewFooterButtonSize
-            Layout.preferredHeight: JamiTheme.messageWebViewFooterButtonSize
+            Layout.preferredWidth: scale * JamiTheme.chatViewFooterButtonSize
+            Layout.preferredHeight: JamiTheme.chatViewFooterButtonSize
 
-            radius: JamiTheme.messageWebViewFooterButtonRadius
-            preferredSize: JamiTheme.messageWebViewFooterButtonIconSize - 6
+            radius: JamiTheme.chatViewFooterButtonRadius
+            preferredSize: JamiTheme.chatViewFooterButtonIconSize - 6
 
             toolTipText: JamiStrings.send
 
diff --git a/src/mainview/components/MessageBarTextArea.qml b/src/mainview/components/MessageBarTextArea.qml
index de2a6b699..8f96ca742 100644
--- a/src/mainview/components/MessageBarTextArea.qml
+++ b/src/mainview/components/MessageBarTextArea.qml
@@ -136,7 +136,7 @@ Flickable {
         //     Shift + Enter -> Next Line
         Keys.onPressed: function (keyEvent) {
             if (keyEvent.matches(StandardKey.Paste)) {
-                MessagesAdapter.pasteKeyDetected()
+                MessagesAdapter.onPaste()
                 keyEvent.accepted = true
             } else if (keyEvent.key === Qt.Key_Enter ||
                        keyEvent.key === Qt.Key_Return) {
diff --git a/src/mainview/components/MessageListView.qml b/src/mainview/components/MessageListView.qml
new file mode 100644
index 000000000..2bc9a68fb
--- /dev/null
+++ b/src/mainview/components/MessageListView.qml
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import net.jami.Models 1.1
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+
+import "../../commoncomponents"
+
+ListView {
+    id: root
+
+    // fade-in mechanism
+    Component.onCompleted: fadeAnimation.start()
+    Rectangle {
+        id: overlay
+        anchors.fill: parent
+        color: JamiTheme.chatviewBgColor
+        visible: opacity !== 0
+        SequentialAnimation {
+            id: fadeAnimation
+            NumberAnimation {
+                target: overlay; property: "opacity"
+                to: 1; duration: 0
+            }
+            NumberAnimation {
+                target: overlay; property: "opacity"
+                to: 0; duration: 240
+            }
+        }
+    }
+    Connections {
+        target: CurrentConversation
+        function onIdChanged() { fadeAnimation.start() }
+    }
+
+    topMargin: 12
+    bottomMargin: 6
+    spacing: 2
+    anchors.centerIn: parent
+    height: parent.height
+    width: parent.width
+    displayMarginBeginning: 2048
+    displayMarginEnd: 2048
+    maximumFlickVelocity: 2048
+    verticalLayoutDirection: ListView.BottomToTop
+    clip: true
+    boundsBehavior: Flickable.StopAtBounds
+    currentIndex: -1
+
+    ScrollBar.vertical: ScrollBar {}
+
+    model: MessagesAdapter.messageListModel
+
+    delegate: MessageDelegate {}
+
+    function getDistanceToBottom() {
+        const scrollDiff = ScrollBar.vertical.position -
+                         (1.0 - ScrollBar.vertical.size)
+        return Math.abs(scrollDiff) * contentHeight
+    }
+
+    onAtYBeginningChanged: loadMoreMsgsIfNeeded()
+
+    function loadMoreMsgsIfNeeded() {
+        if (atYBeginning && !CurrentConversation.allMessagesLoaded)
+            MessagesAdapter.loadMoreMessages()
+    }
+
+    Connections {
+        target: MessagesAdapter
+
+        function onNewInteraction() {
+            if (root.getDistanceToBottom() < 80 &&
+                    !root.atYEnd) {
+                Qt.callLater(root.positionViewAtBeginning)
+            }
+        }
+
+        function onMoreMessagesLoaded() {
+            if (root.contentHeight < root.height) {
+                root.loadMoreMsgsIfNeeded()
+            }
+        }
+    }
+}
diff --git a/src/mainview/components/MessageWebView.qml b/src/mainview/components/MessageWebView.qml
deleted file mode 100644
index a366e569b..000000000
--- a/src/mainview/components/MessageWebView.qml
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- * Copyright (C) 2020 by Savoir-faire Linux
- * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-import QtQuick 2.15
-import QtQuick.Controls 2.15
-import QtQuick.Layouts 1.15
-import QtWebEngine 1.10
-import QtWebChannel 1.15
-
-import net.jami.Models 1.1
-import net.jami.Adapters 1.1
-import net.jami.Constants 1.1
-
-import "../../commoncomponents"
-import "../js/pluginhandlerpickercreation.js" as PluginHandlerPickerCreation
-
-Rectangle {
-    id: root
-
-    property string headerUserAliasLabelText: ""
-    property string headerUserUserNameLabelText: ""
-    property bool jsLoaded: false
-
-    signal needToHideConversationInCall
-    signal messagesCleared
-    signal messagesLoaded
-
-    function setSendMessageContent(content) {
-        jsBridgeObject.setSendMessageContentRequest(content)
-    }
-
-    function focusMessageWebView() {
-        messageWebViewFooter.textInput.forceActiveFocus()
-    }
-
-    function webViewRunJavaScript(arg) {
-        messageWebView.runJavaScript(arg)
-    }
-
-    function updateChatviewTheme() {
-        var theme = 'setTheme("\
-            --svg-invert-percentage:' + JamiTheme.invertPercentageInDecimal + ';\
-            --jami-light-blue:' + JamiTheme.jamiLightBlue + ';\
-            --jami-dark-blue: ' + JamiTheme.jamiDarkBlue + ';\
-            --text-color: ' + JamiTheme.chatviewTextColor + ';\
-            --timestamp-color:' + JamiTheme.timestampColor + ';\
-            --message-out-bg:' + JamiTheme.messageOutBgColor + ';\
-            --message-out-txt:' + JamiTheme.messageOutTxtColor + ';\
-            --message-in-bg:' + JamiTheme.messageInBgColor + ';\
-            --message-in-txt:' + JamiTheme.messageInTxtColor + ';\
-            --file-in-timestamp-color:' + JamiTheme.fileOutTimestampColor + ';\
-            --file-out-timestamp-color:' + JamiTheme.fileInTimestampColor + ';\
-            --bg-color:' + JamiTheme.chatviewBgColor + ';\
-            --action-icon-color:' + JamiTheme.chatviewButtonColor + ';\
-            --action-icon-hover-color:' + JamiTheme.hoveredButtonColor + ';\
-            --action-icon-press-color:' + JamiTheme.pressedButtonColor + ';\
-            --placeholder-text-color:' + JamiTheme.placeholderTextColor + ';\
-            --invite-hover-color:' + JamiTheme.inviteHoverColor + ';\
-            --bg-text-input:' + JamiTheme.bgTextInput + ';\
-            --bg-invitation-rect:' + JamiTheme.bgInvitationRectColor + ';\
-            --preview-text-container-color:' + JamiTheme.previewTextContainerColor + ';\
-            --preview-title-color:' + JamiTheme.previewTitleColor + ';\
-            --preview-subtitle-color:' + JamiTheme.previewSubtitleColor + ';\
-            --preview-image-background-color:' + JamiTheme.previewImageBackgroundColor + ';\
-            --preview-card-container-color:' + JamiTheme.previewCardContainerColor + ';\
-            --preview-url-color:' + JamiTheme.previewUrlColor + ';")'
-        messageWebView.runJavaScript("init_picker(" + JamiTheme.darkTheme + ");")
-        messageWebView.runJavaScript(theme);
-    }
-
-    color: JamiTheme.primaryBackgroundColor
-
-    Connections {
-        target: JamiTheme
-
-        function onDarkThemeChanged() {
-            updateChatviewTheme()
-        }
-    }
-
-    QtObject {
-        id: jsBridgeObject
-
-        // ID, under which this object will be known at chatview.js side.
-        WebChannel.id: "jsbridge"
-
-        // signals to trigger functions in chatview.js
-        // mainly used to avoid input arg string escape
-        signal setSendMessageContentRequest(string content)
-
-        // Functions that are exposed, return code can be derived from js side
-        // by setting callback function.
-        function deleteInteraction(arg) {
-            MessagesAdapter.deleteInteraction(arg)
-        }
-
-        function retryInteraction(arg) {
-            MessagesAdapter.retryInteraction(arg)
-        }
-
-        function openFile(arg) {
-            MessagesAdapter.openFile(arg)
-        }
-
-        function acceptFile(arg) {
-            MessagesAdapter.acceptFile(arg)
-        }
-
-        function refuseFile(arg) {
-            MessagesAdapter.refuseFile(arg)
-        }
-
-        function emitMessagesCleared() {
-            root.messagesCleared()
-        }
-
-        function emitMessagesLoaded() {
-            root.messagesLoaded()
-        }
-
-        function copyToDownloads(interactionId, displayName) {
-            MessagesAdapter.copyToDownloads(interactionId, displayName)
-        }
-
-        function parseI18nData() {
-            return MessagesAdapter.chatviewTranslatedStrings
-        }
-
-        function loadMessages(n) {
-            return MessagesAdapter.loadMessages(n)
-        }
-    }
-
-    ColumnLayout {
-        anchors.fill: root
-
-        spacing: 0
-
-        MessageWebViewHeader {
-            id: messageWebViewHeader
-
-            Layout.alignment: Qt.AlignHCenter
-            Layout.fillWidth: true
-            Layout.preferredHeight: JamiTheme.messageWebViewHeaderPreferredHeight
-            Layout.maximumHeight: JamiTheme.messageWebViewHeaderPreferredHeight
-
-            userAliasLabelText: headerUserAliasLabelText
-            userUserNameLabelText: headerUserUserNameLabelText
-
-            DropArea {
-                anchors.fill: parent
-                onDropped: messageWebViewFooter.setFilePathsToSend(drop.urls)
-            }
-
-            onBackClicked: {
-                mainView.showWelcomeView()
-            }
-
-            onNeedToHideConversationInCall: {
-                root.needToHideConversationInCall()
-            }
-
-            onPluginSelector: {
-                // Create plugin handler picker - PLUGINS
-                PluginHandlerPickerCreation.createPluginHandlerPickerObjects(
-                            root, false)
-                PluginHandlerPickerCreation.calculateCurrentGeo(root.width / 2,
-                                                                root.height / 2)
-                PluginHandlerPickerCreation.openPluginHandlerPicker()
-            }
-        }
-
-        StackLayout {
-            id: messageWebViewStack
-
-            Layout.alignment: Qt.AlignHCenter
-            Layout.fillWidth: true
-            Layout.fillHeight: true
-            Layout.topMargin: JamiTheme.messageWebViewHairLineSize
-            Layout.bottomMargin: JamiTheme.messageWebViewHairLineSize
-
-            currentIndex: CurrentConversation.isRequest || CurrentConversation.needsSyncing
-
-            GeneralWebEngineView {
-                id: messageWebView
-
-                Layout.fillWidth: true
-                Layout.fillHeight: true
-
-                onCompletedLoadHtml: ":/chatview.html"
-
-                webChannel.registeredObjects: [jsBridgeObject]
-
-                DropArea {
-                    anchors.fill: parent
-                    onDropped: messageWebViewFooter.setFilePathsToSend(drop.urls)
-                }
-
-                onLoadingChanged: {
-                    if (loadRequest.status == WebEngineView.LoadSucceededStatus) {
-                        messageWebView.runJavaScript(UtilsAdapter.getStyleSheet(
-                                                         "chatcss",
-                                                         UtilsAdapter.qStringFromFile(
-                                                             ":/chatview.css")))
-                        messageWebView.runJavaScript(UtilsAdapter.getStyleSheet(
-                                                         "chatwin",
-                                                         UtilsAdapter.qStringFromFile(
-                                                             ":/chatview-qt.css")))
-                        messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
-                                                         ":/linkify.js"))
-                        messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
-                                                         ":/linkify-html.js"))
-                        messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
-                                                         ":/linkify-string.js"))
-                        messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
-                                                         ":/qwebchannel.js"))
-                        messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
-                                                         ":/jed.js"))
-                        messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
-                                                         ":/emoji.js"))
-                        messageWebView.runJavaScript(UtilsAdapter.qStringFromFile(
-                                                         ":/previewInfo.js"))
-                        messageWebView.runJavaScript(
-                                    UtilsAdapter.qStringFromFile(":/chatview.js"),
-                                    function() {
-                                        messageWebView.runJavaScript("init_i18n();")
-                                        MessagesAdapter.setDisplayLinks()
-                                        updateChatviewTheme()
-                                        messageWebView.runJavaScript("displayNavbar(false);")
-                                        messageWebView.runJavaScript("hideMessageBar(true);")
-                                        jsLoaded = true
-                                    })
-                    }
-                }
-            }
-
-            InvitationView {
-                id: invitationView
-
-                Layout.fillWidth: true
-                Layout.fillHeight: true
-            }
-        }
-
-        ReadOnlyFooter {
-            visible: CurrentConversation.readOnly
-            Layout.fillWidth: true
-        }
-
-        MessageWebViewFooter {
-            id: messageWebViewFooter
-
-            visible: {
-                if (CurrentConversation.needsSyncing || CurrentConversation.readOnly)
-                    return false
-                else if (CurrentConversation.isSwarm && CurrentConversation.isRequest)
-                    return false
-                return true
-            }
-
-            Layout.alignment: Qt.AlignHCenter
-            Layout.fillWidth: true
-            Layout.preferredHeight: implicitHeight
-            Layout.maximumHeight: JamiTheme.messageWebViewFooterMaximumHeight
-
-            DropArea {
-                anchors.fill: parent
-                onDropped: messageWebViewFooter.setFilePathsToSend(drop.urls)
-            }
-        }
-    }
-}
diff --git a/src/mainview/components/MessageWebViewHeader.qml b/src/mainview/components/MessageWebViewHeader.qml
index fd30b4930..de14a33f1 100644
--- a/src/mainview/components/MessageWebViewHeader.qml
+++ b/src/mainview/components/MessageWebViewHeader.qml
@@ -223,7 +223,7 @@ Rectangle {
         lBorderwidth: 0
         rBorderwidth: 0
         tBorderwidth: 0
-        bBorderwidth: JamiTheme.messageWebViewHairLineSize
+        bBorderwidth: JamiTheme.chatViewHairLineSize
         borderColor: JamiTheme.tabbarBorderColor
     }
 }
diff --git a/src/mainview/components/ReadOnlyFooter.qml b/src/mainview/components/ReadOnlyFooter.qml
index 5869e541f..97033315e 100644
--- a/src/mainview/components/ReadOnlyFooter.qml
+++ b/src/mainview/components/ReadOnlyFooter.qml
@@ -34,7 +34,7 @@ Control {
 
         Rectangle {
             anchors.top: parent.top
-            height: JamiTheme.messageWebViewHairLineSize
+            height: JamiTheme.chatViewHairLineSize
             width: parent.width
             color: JamiTheme.tabbarBorderColor
         }
diff --git a/src/messagesadapter.cpp b/src/messagesadapter.cpp
index ec68a5cea..fb6d7ef2b 100644
--- a/src/messagesadapter.cpp
+++ b/src/messagesadapter.cpp
@@ -1,4 +1,4 @@
-/*!
+/*
  * Copyright (C) 2020 by Savoir-faire Linux
  * Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
  * Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
@@ -26,7 +26,6 @@
 #include "appsettingsmanager.h"
 #include "qtutils.h"
 #include "utils.h"
-#include "webchathelpers.h"
 
 #include <api/datatransfermodel.h>
 
@@ -39,13 +38,30 @@
 #include <QUrl>
 #include <QMimeData>
 #include <QBuffer>
+#include <QtMath>
 
 MessagesAdapter::MessagesAdapter(AppSettingsManager* settingsManager,
+                                 PreviewEngine* previewEngine,
                                  LRCInstance* instance,
                                  QObject* parent)
     : QmlAdapterBase(instance, parent)
     , settingsManager_(settingsManager)
-{}
+    , previewEngine_(previewEngine)
+    , filteredMsgListModel_(new FilteredMsgListModel(this))
+{
+    connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
+        const QString& convId = lrcInstance_->get_selectedConvUid();
+        const auto& conversation = lrcInstance_->getConversationFromConvUid(convId);
+        filteredMsgListModel_->setSourceModel(conversation.interactions.get());
+        set_messageListModel(QVariant::fromValue(filteredMsgListModel_));
+    });
+
+    connect(previewEngine_, &PreviewEngine::infoReady, this, &MessagesAdapter::onPreviewInfoReady);
+    connect(previewEngine_,
+            &PreviewEngine::linkifyReady,
+            this,
+            &MessagesAdapter::onMessageLinkified);
+}
 
 void
 MessagesAdapter::safeInit()
@@ -59,71 +75,26 @@ MessagesAdapter::safeInit()
 void
 MessagesAdapter::setupChatView(const QVariantMap& convInfo)
 {
-    Utils::oneShotConnect(qmlObj_, SIGNAL(messagesCleared()), this, SLOT(slotMessagesCleared()));
-    setMessagesVisibility(false);
-    clearChatView();
-    setIsSwarm(convInfo["isSwarm"].toBool());
+    auto* convModel = lrcInstance_->getCurrentConversationModel();
+    auto convId = convInfo["convId"].toString();
+    if (convInfo["isSwarm"].toBool()) {
+        convModel->loadConversationMessages(convId, loadChunkSize_);
+    }
 
+    // TODO: current conv observe
     Q_EMIT newMessageBarPlaceholderText(convInfo["title"].toString());
 }
 
 void
-MessagesAdapter::onNewInteraction(const QString& convUid,
-                                  const QString& interactionId,
-                                  const lrc::api::interaction::Info& interaction)
+MessagesAdapter::loadMoreMessages()
 {
     auto accountId = lrcInstance_->get_currentAccountId();
-    newInteraction(accountId, convUid, interactionId, interaction);
-}
-
-void
-MessagesAdapter::onInteractionStatusUpdated(const QString& convUid,
-                                            const QString& interactionId,
-                                            const lrc::api::interaction::Info& interaction)
-{
-    auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
-    updateInteraction(*currentConversationModel, interactionId, interaction);
-}
-
-void
-MessagesAdapter::onInteractionRemoved(const QString& convUid, const QString& interactionId)
-{
-    Q_UNUSED(convUid);
-    removeInteraction(interactionId);
-}
-
-void
-MessagesAdapter::onNewMessagesAvailable(const QString& accountId, const QString& conversationId)
-{
-    auto* convModel = lrcInstance_->accountModel().getAccountInfo(accountId).conversationModel.get();
-    auto optConv = convModel->getConversationForUid(conversationId);
-    if (!optConv)
-        return;
-    updateHistory(*convModel, optConv->get().interactions, optConv->get().allMessagesLoaded);
-    Utils::oneShotConnect(qmlObj_, SIGNAL(messagesLoaded()), this, SLOT(slotMessagesLoaded()));
-}
-
-void
-MessagesAdapter::updateConversation(const QString& conversationId)
-{
-    if (conversationId != lrcInstance_->get_selectedConvUid())
-        return;
-    auto* convModel = lrcInstance_->getCurrentConversationModel();
-    if (auto optConv = convModel->getConversationForUid(conversationId))
-        setConversationProfileData(optConv->get());
-}
-
-void
-MessagesAdapter::onComposingStatusChanged(const QString& convId,
-                                          const QString& contactUri,
-                                          bool isComposing)
-{
-    if (convId != lrcInstance_->get_selectedConvUid())
-        return;
-    if (!settingsManager_->getValue(Settings::Key::EnableTypingIndicator).toBool()) {
-        return;
+    auto convId = lrcInstance_->get_selectedConvUid();
+    const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId, accountId);
+    if (convInfo.isSwarm()) {
+        auto* convModel = lrcInstance_->getCurrentConversationModel();
+        convModel->loadConversationMessages(convId, loadChunkSize_);
     }
-    contactIsComposing(contactUri, isComposing);
 }
 
 void
@@ -138,33 +109,9 @@ MessagesAdapter::connectConversationModel()
                      Qt::UniqueConnection);
 
     QObject::connect(currentConversationModel,
-                     &ConversationModel::interactionStatusUpdated,
+                     &ConversationModel::conversationMessagesLoaded,
                      this,
-                     &MessagesAdapter::onInteractionStatusUpdated,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::interactionRemoved,
-                     this,
-                     &MessagesAdapter::onInteractionRemoved,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::newMessagesAvailable,
-                     this,
-                     &MessagesAdapter::onNewMessagesAvailable,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::conversationReady,
-                     this,
-                     &MessagesAdapter::updateConversation,
-                     Qt::UniqueConnection);
-
-    QObject::connect(currentConversationModel,
-                     &ConversationModel::composingStatusChanged,
-                     this,
-                     &MessagesAdapter::onComposingStatusChanged,
+                     &MessagesAdapter::onConversationMessagesLoaded,
                      Qt::UniqueConnection);
 }
 
@@ -174,29 +121,6 @@ MessagesAdapter::sendConversationRequest()
     lrcInstance_->makeConversationPermanent();
 }
 
-void
-MessagesAdapter::slotMessagesCleared()
-{
-    auto* convModel = lrcInstance_->getCurrentConversationModel();
-
-    auto optConv = convModel->getConversationForUid(lrcInstance_->get_selectedConvUid());
-    if (!optConv)
-        return;
-    if (optConv->get().isSwarm() && !optConv->get().allMessagesLoaded) {
-        convModel->loadConversationMessages(optConv->get().uid, 20);
-    } else {
-        updateHistory(*convModel, optConv->get().interactions, optConv->get().allMessagesLoaded);
-        Utils::oneShotConnect(qmlObj_, SIGNAL(messagesLoaded()), this, SLOT(slotMessagesLoaded()));
-    }
-    setConversationProfileData(optConv->get());
-}
-
-void
-MessagesAdapter::slotMessagesLoaded()
-{
-    setMessagesVisibility(true);
-}
-
 void
 MessagesAdapter::sendMessage(const QString& message)
 {
@@ -279,7 +203,7 @@ MessagesAdapter::refuseFile(const QString& interactionId)
 }
 
 void
-MessagesAdapter::pasteKeyDetected()
+MessagesAdapter::onPaste()
 {
     const QMimeData* mimeData = QApplication::clipboard()->mimeData();
 
@@ -328,186 +252,24 @@ MessagesAdapter::userIsComposing(bool isComposing)
 }
 
 void
-MessagesAdapter::setConversationProfileData(const conversation::Info& convInfo)
-{
-    // make the all the participant avatars available within the web view
-    for (const auto& participant : convInfo.participants) {
-        QByteArray ba;
-        QBuffer bu(&ba);
-        Utils::conversationAvatar(lrcInstance_, convInfo.uid).save(&bu, "PNG");
-        setSenderImage(participant, QString::fromLocal8Bit(ba.toBase64()));
-    }
-}
-
-void
-MessagesAdapter::newInteraction(const QString& accountId,
-                                const QString& convUid,
-                                const QString& interactionId,
-                                const interaction::Info& interaction)
+MessagesAdapter::onNewInteraction(const QString& convUid,
+                                  const QString& interactionId,
+                                  const interaction::Info& interaction)
 {
     Q_UNUSED(interactionId);
     try {
         if (convUid.isEmpty() || convUid != lrcInstance_->get_selectedConvUid()) {
             return;
         }
+        auto accountId = lrcInstance_->get_currentAccountId();
         auto& accountInfo = lrcInstance_->getAccountInfo(accountId);
         auto& convModel = accountInfo.conversationModel;
         convModel->clearUnreadInteractions(convUid);
-        printNewInteraction(*convModel, interactionId, interaction);
         Q_EMIT newInteraction(static_cast<int>(interaction.type));
     } catch (...) {
     }
 }
 
-/*
- * JS invoke.
- */
-void
-MessagesAdapter::setMessagesVisibility(bool visible)
-{
-    QString s = QString::fromLatin1(visible ? "showMessagesDiv();" : "hideMessagesDiv();");
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-}
-
-void
-MessagesAdapter::setIsSwarm(bool isSwarm)
-{
-    QString s = QString::fromLatin1("set_is_swarm(%1)").arg(isSwarm);
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-}
-
-void
-MessagesAdapter::clearChatView()
-{
-    QString s = QString::fromLatin1("clearMessages();");
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-}
-
-void
-MessagesAdapter::setDisplayLinks()
-{
-    QString s
-        = QString::fromLatin1("setDisplayLinks(%1);")
-              .arg(settingsManager_->getValue(Settings::Key::DisplayHyperlinkPreviews).toBool());
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-}
-
-void
-MessagesAdapter::updateHistory(lrc::api::ConversationModel& conversationModel,
-                               MessagesList interactions,
-                               bool allLoaded)
-{
-    auto conversationId = lrcInstance_->get_selectedConvUid();
-    auto interactionsStr
-        = interactionsToJsonArrayObject(conversationModel, conversationId, interactions).toUtf8();
-    QString s;
-    QTextStream out(&s);
-    out << "updateHistory(" << interactionsStr << ", " << (allLoaded? "true" : "false") << ");";
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-    conversationModel.clearUnreadInteractions(conversationId);
-}
-
-void
-MessagesAdapter::setSenderImage(const QString& sender, const QString& senderImage)
-{
-    QJsonObject setSenderImageObject = QJsonObject();
-    setSenderImageObject.insert("sender_contact_method", QJsonValue(sender));
-    setSenderImageObject.insert("sender_image", QJsonValue(senderImage));
-
-    auto setSenderImageObjectString = QString(
-        QJsonDocument(setSenderImageObject).toJson(QJsonDocument::Compact));
-    QString s = QString::fromLatin1("setSenderImage(%1);")
-                    .arg(setSenderImageObjectString.toUtf8().constData());
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-}
-
-void
-MessagesAdapter::printNewInteraction(lrc::api::ConversationModel& conversationModel,
-                                     const QString& msgId,
-                                     const lrc::api::interaction::Info& interaction)
-{
-    auto interactionObject = interactionToJsonInteractionObject(conversationModel,
-                                                                lrcInstance_->get_selectedConvUid(),
-                                                                msgId,
-                                                                interaction)
-                                 .toUtf8();
-    if (interactionObject.isEmpty()) {
-        return;
-    }
-    QString s = QString::fromLatin1("addMessage(%1);").arg(interactionObject.constData());
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-}
-
-void
-MessagesAdapter::updateInteraction(lrc::api::ConversationModel& conversationModel,
-                                   const QString& msgId,
-                                   const lrc::api::interaction::Info& interaction)
-{
-    auto interactionObject = interactionToJsonInteractionObject(conversationModel,
-                                                                lrcInstance_->get_selectedConvUid(),
-                                                                msgId,
-                                                                interaction)
-                                 .toUtf8();
-    if (interactionObject.isEmpty()) {
-        return;
-    }
-    QString s = QString::fromLatin1("updateMessage(%1);").arg(interactionObject.constData());
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-}
-
-void
-MessagesAdapter::setMessagesImageContent(const QString& path, bool isBased64)
-{
-    if (isBased64) {
-        QString param = QString("addImage_base64('%1')").arg(path);
-        QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
-    } else {
-        QString param = QString("addImage_path('file://%1')").arg(path);
-        QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
-    }
-}
-
-void
-MessagesAdapter::setMessagesFileContent(const QString& path)
-{
-    qint64 fileSize = QFileInfo(path).size();
-    QString fileName = QFileInfo(path).fileName();
-
-    QString param = QString("addFile_path('%1','%2','%3')")
-                        .arg(path, fileName, Utils::humanFileSize(fileSize));
-
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, param));
-}
-
-void
-MessagesAdapter::removeInteraction(const QString& interactionId)
-{
-    QString s = QString::fromLatin1("removeInteraction(%1);").arg(interactionId);
-    QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-}
-
-void
-MessagesAdapter::setSendMessageContent(const QString& content)
-{
-    QMetaObject::invokeMethod(qmlObj_, "setSendMessageContent", Q_ARG(QVariant, content));
-}
-
-void
-MessagesAdapter::contactIsComposing(const QString& contactUri, bool isComposing)
-{
-    auto* convModel = lrcInstance_->getCurrentConversationModel();
-    auto convInfo = convModel->getConversationForUid(lrcInstance_->get_selectedConvUid());
-    if (!convInfo)
-        return;
-    auto& conv = convInfo->get();
-    bool showIsComposing = conv.participants.first() == contactUri;
-    if (showIsComposing) {
-        QString s
-            = QString::fromLatin1("showTypingIndicator(`%1`, %2);").arg(contactUri).arg(isComposing);
-        QMetaObject::invokeMethod(qmlObj_, "webViewRunJavaScript", Q_ARG(QVariant, s));
-    }
-}
-
 void
 MessagesAdapter::acceptInvitation(const QString& convId)
 {
@@ -577,12 +339,75 @@ MessagesAdapter::removeContact(const QString& convUid, bool banContact)
 }
 
 void
-MessagesAdapter::loadMessages(int n)
+MessagesAdapter::onPreviewInfoReady(QString messageId, QVariantMap info)
 {
-    auto* convModel = lrcInstance_->getCurrentConversationModel();
-    auto convOpt = convModel->getConversationForUid(lrcInstance_->get_selectedConvUid());
-    if (!convOpt)
+    const QString& convId = lrcInstance_->get_selectedConvUid();
+    const QString& accId = lrcInstance_->get_currentAccountId();
+    auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
+    conversation.interactions->addHyperlinkInfo(messageId, info);
+}
+
+void
+MessagesAdapter::onConversationMessagesLoaded(uint32_t, const QString& convId)
+{
+    if (convId != lrcInstance_->get_selectedConvUid())
         return;
-    if (convOpt->get().isSwarm() && !convOpt->get().allMessagesLoaded)
-        convModel->loadConversationMessages(convOpt->get().uid, n);
+    Q_EMIT moreMessagesLoaded();
+}
+
+void
+MessagesAdapter::parseMessageUrls(const QString& messageId, const QString& msg)
+{
+    previewEngine_->parseMessage(messageId, msg);
+}
+
+void
+MessagesAdapter::onMessageLinkified(const QString& messageId, const QString& linkified)
+{
+    const QString& convId = lrcInstance_->get_selectedConvUid();
+    const QString& accId = lrcInstance_->get_currentAccountId();
+    auto& conversation = lrcInstance_->getConversationFromConvUid(convId, accId);
+    conversation.interactions->linkifyMessage(messageId, linkified);
+}
+
+bool
+MessagesAdapter::isImage(const QString& message)
+{
+    QRegularExpression pattern("[^\\s]+(.*?)\\.(jpg|jpeg|png)$",
+                               QRegularExpression::CaseInsensitiveOption);
+    QRegularExpressionMatch match = pattern.match(message);
+    return match.hasMatch();
+}
+
+bool
+MessagesAdapter::isAnimatedImage(const QString& msg)
+{
+    QRegularExpression pattern("[^\\s]+(.*?)\\.(gif|apng|webp|avif|flif)$",
+                               QRegularExpression::CaseInsensitiveOption);
+    QRegularExpressionMatch match = pattern.match(msg);
+    return match.hasMatch();
+}
+
+QString
+MessagesAdapter::getFormattedTime(const quint64 timestamp)
+{
+    const auto now = QDateTime::currentDateTime();
+    const auto seconds = now.toSecsSinceEpoch() - timestamp;
+    auto interval = qFloor(seconds / (3600 * 24));
+    if (interval > 5)
+        return QLocale::system().toString(QDateTime::fromSecsSinceEpoch(timestamp),
+                                          QLocale::ShortFormat);
+    if (interval > 1)
+        return QObject::tr("%1 days ago").arg(interval);
+    if (interval == 1)
+        return QObject::tr("one day ago");
+    interval = qFloor(seconds / 3600);
+    if (interval > 1)
+        return QObject::tr("%1 hours ago").arg(interval);
+    if (interval == 1)
+        return QObject::tr("one hour ago");
+    interval = qFloor(seconds / 60);
+    if (interval > 1)
+        return QObject::tr("%1 minutes ago").arg(interval);
+    return QObject::tr("just now");
 }
diff --git a/src/messagesadapter.h b/src/messagesadapter.h
index 278393d58..fd27ac9b1 100644
--- a/src/messagesadapter.h
+++ b/src/messagesadapter.h
@@ -1,4 +1,4 @@
-/*!
+/*
  * Copyright (C) 2020 by Savoir-faire Linux
  * Author: Mingrui Zhang   <mingrui.zhang@savoirfairelinux.com>
  *
@@ -20,28 +20,66 @@
 
 #include "lrcinstance.h"
 #include "qmladapterbase.h"
+#include "previewengine.h"
+
 #include "api/chatview.h"
 
 #include <QObject>
 #include <QString>
 
+#include <QSortFilterProxyModel>
+
+class FilteredMsgListModel final : public QSortFilterProxyModel
+{
+    Q_OBJECT
+
+public:
+    explicit FilteredMsgListModel(QObject* parent = nullptr)
+        : QSortFilterProxyModel(parent)
+    {
+        sort(0, Qt::AscendingOrder);
+    }
+
+    bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
+    {
+        auto index = sourceModel()->index(sourceRow, 0, sourceParent);
+        auto type = sourceModel()->data(index, MessageList::Role::Type).toInt();
+        return static_cast<interaction::Type>(type) != interaction::Type::MERGE;
+    };
+
+    bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
+    {
+        return left.row() > right.row();
+    };
+};
+
 class AppSettingsManager;
 
 class MessagesAdapter final : public QmlAdapterBase
 {
     Q_OBJECT
-    Q_PROPERTY(QVariantMap chatviewTranslatedStrings MEMBER chatviewTranslatedStrings_ CONSTANT)
+    QML_RO_PROPERTY(QVariant, messageListModel)
 
 public:
     explicit MessagesAdapter(AppSettingsManager* settingsManager,
+                             PreviewEngine* previewEngine,
                              LRCInstance* instance,
                              QObject* parent = nullptr);
     ~MessagesAdapter() = default;
 
+Q_SIGNALS:
+    void newInteraction(int type);
+    void newMessageBarPlaceholderText(QString placeholderText);
+    void newFilePasted(QString filePath);
+    void newTextPasted();
+    void previewInformationToQML(QString messageId, QStringList previewInformation);
+    void moreMessagesLoaded();
+
 protected:
     void safeInit() override;
 
     Q_INVOKABLE void setupChatView(const QVariantMap& convInfo);
+    Q_INVOKABLE void loadMoreMessages();
     Q_INVOKABLE void connectConversationModel();
     Q_INVOKABLE void sendConversationRequest();
     Q_INVOKABLE void removeConversation(const QString& convUid);
@@ -51,70 +89,39 @@ protected:
     Q_INVOKABLE void refuseInvitation(const QString& convUid = "");
     Q_INVOKABLE void blockConversation(const QString& convUid = "");
     Q_INVOKABLE void unbanContact(int index);
-
-    // JS Q_INVOKABLE.
-    Q_INVOKABLE void setDisplayLinks();
     Q_INVOKABLE void sendMessage(const QString& message);
     Q_INVOKABLE void sendFile(const QString& message);
-    Q_INVOKABLE void retryInteraction(const QString& interactionId);
-    Q_INVOKABLE void deleteInteraction(const QString& interactionId);
-    Q_INVOKABLE void openUrl(const QString& url);
-    Q_INVOKABLE void openFile(const QString& arg);
     Q_INVOKABLE void acceptFile(const QString& arg);
     Q_INVOKABLE void refuseFile(const QString& arg);
-    Q_INVOKABLE void pasteKeyDetected();
-    Q_INVOKABLE void userIsComposing(bool isComposing);
-    Q_INVOKABLE void loadMessages(int n);
+    Q_INVOKABLE void openUrl(const QString& url);
+    Q_INVOKABLE void openFile(const QString& arg);
+    Q_INVOKABLE void retryInteraction(const QString& interactionId);
+    Q_INVOKABLE void deleteInteraction(const QString& interactionId);
     Q_INVOKABLE void copyToDownloads(const QString& interactionId, const QString& displayName);
+    Q_INVOKABLE void userIsComposing(bool isComposing);
+    Q_INVOKABLE bool isImage(const QString& msg);
+    Q_INVOKABLE bool isAnimatedImage(const QString& msg);
+    Q_INVOKABLE QString getFormattedTime(const quint64 timestamp);
+    Q_INVOKABLE void parseMessageUrls(const QString& messageId, const QString& msg);
+    Q_INVOKABLE void onPaste();
 
     // Run corrsponding js functions, c++ to qml.
-    void setMessagesVisibility(bool visible);
-    void setIsSwarm(bool isSwarm);
-    void clearChatView();
-    void updateHistory(ConversationModel& conversationModel,
-                       MessagesList interactions,
-                       bool allLoaded);
-    void setSenderImage(const QString& sender, const QString& senderImage);
-    void printNewInteraction(lrc::api::ConversationModel& conversationModel,
-                             const QString& msgId,
-                             const lrc::api::interaction::Info& interaction);
-    void updateInteraction(lrc::api::ConversationModel& conversationModel,
-                           const QString& msgId,
-                           const lrc::api::interaction::Info& interaction);
     void setMessagesImageContent(const QString& path, bool isBased64 = false);
     void setMessagesFileContent(const QString& path);
-    void removeInteraction(const QString& interactionId);
     void setSendMessageContent(const QString& content);
-    void contactIsComposing(const QString& contactUri, bool isComposing);
-
-Q_SIGNALS:
-    void newInteraction(int type);
-    void newMessageBarPlaceholderText(QString placeholderText);
-    void newFilePasted(QString filePath);
-    void newTextPasted();
 
 private Q_SLOTS:
-    void slotMessagesCleared();
-    void slotMessagesLoaded();
     void onNewInteraction(const QString& convUid,
                           const QString& interactionId,
                           const interaction::Info& interaction);
-    void onInteractionStatusUpdated(const QString& convUid,
-                                    const QString& interactionId,
-                                    const interaction::Info& interaction);
-    void onInteractionRemoved(const QString& convUid, const QString& interactionId);
-    void onNewMessagesAvailable(const QString& accountId, const QString& conversationId);
-    void updateConversation(const QString& conversationId);
-    void onComposingStatusChanged(const QString& uid, const QString& contactUri, bool isComposing);
+    void onPreviewInfoReady(QString messageIndex, QVariantMap urlInMessage);
+    void onConversationMessagesLoaded(uint32_t requestId, const QString& convId);
+    void onMessageLinkified(const QString& messageId, const QString& linkified);
 
 private:
-    void setConversationProfileData(const lrc::api::conversation::Info& convInfo);
-    void newInteraction(const QString& accountId,
-                        const QString& convUid,
-                        const QString& interactionId,
-                        const interaction::Info& interaction);
-
-    const QVariantMap chatviewTranslatedStrings_ {lrc::api::chatview::getTranslatedStrings()};
-
     AppSettingsManager* settingsManager_;
+    PreviewEngine* previewEngine_;
+    FilteredMsgListModel* filteredMsgListModel_;
+
+    static constexpr const int loadChunkSize_ {20};
 };
diff --git a/src/previewengine.cpp b/src/previewengine.cpp
new file mode 100644
index 000000000..439a91c59
--- /dev/null
+++ b/src/previewengine.cpp
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "previewengine.h"
+
+#include <QtWebEngine>
+#include <QWebEngineScript>
+#include <QWebEngineProfile>
+#include <QWebEngineSettings>
+
+PreviewEngine::PreviewEngine(QObject* parent)
+    : QWebEngineView(qobject_cast<QWidget*>(parent))
+    , pimpl_(new PreviewEnginePrivate(this))
+{
+    QWebEngineProfile* profile = QWebEngineProfile::defaultProfile();
+
+    QDir dataDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
+    dataDir.cdUp();
+    auto cachePath = dataDir.absolutePath() + "/jami";
+    profile->setCachePath(cachePath);
+    profile->setPersistentStoragePath(cachePath);
+    profile->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
+    profile->setHttpCacheType(QWebEngineProfile::NoCache);
+
+    setPage(new QWebEnginePage(profile, this));
+
+    settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
+    settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::ErrorPageEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::ScreenCaptureEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::LinksIncludedInFocusChain, false);
+    settings()->setAttribute(QWebEngineSettings::LocalStorageEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::AllowRunningInsecureContent, true);
+    settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true);
+    settings()->setAttribute(QWebEngineSettings::XSSAuditingEnabled, false);
+    settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, true);
+
+    setContextMenuPolicy(Qt::ContextMenuPolicy::NoContextMenu);
+
+    channel_ = new QWebChannel(this);
+    channel_->registerObject(QStringLiteral("jsbridge"), pimpl_);
+
+    page()->setWebChannel(channel_);
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify.js"), QWebEngineScript::MainWorld);
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/linkify-string.js"),
+                          QWebEngineScript::MainWorld);
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/qwebchannel.js"),
+                          QWebEngineScript::MainWorld);
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/previewInfo.js"),
+                          QWebEngineScript::MainWorld);
+    page()->runJavaScript(Utils::QByteArrayFromFile(":/misc/previewInterop.js"),
+                          QWebEngineScript::MainWorld);
+}
+
+void
+PreviewEngine::parseMessage(const QString& messageId, const QString& msg)
+{
+    page()->runJavaScript(QString("parseMessage(`%1`, `%2`)").arg(messageId, msg));
+}
+
+void
+PreviewEnginePrivate::log(const QString& str)
+{
+    qDebug() << str;
+}
+
+void
+PreviewEnginePrivate::infoReady(const QString& messageId, const QVariantMap& info)
+{
+    Q_EMIT parent_->infoReady(messageId, info);
+}
+
+void
+PreviewEnginePrivate::linkifyReady(const QString& messageId, const QString& linkified)
+{
+    Q_EMIT parent_->linkifyReady(messageId, linkified);
+}
diff --git a/src/previewengine.h b/src/previewengine.h
new file mode 100644
index 000000000..9b45c6428
--- /dev/null
+++ b/src/previewengine.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "utils.h"
+
+#include <QtWebChannel>
+#include <QtWebEngine>
+#include <QtWebEngineCore>
+#include <QtWebEngine>
+#include <QWebEngineView>
+
+class PreviewEngine;
+
+class PreviewEnginePrivate : public QObject
+{
+    Q_OBJECT
+public:
+    explicit PreviewEnginePrivate(PreviewEngine* parent)
+        : parent_(parent)
+    {}
+
+    Q_INVOKABLE void infoReady(const QString& messageId, const QVariantMap& info);
+    Q_INVOKABLE void linkifyReady(const QString& messageId, const QString& linkified);
+    Q_INVOKABLE void log(const QString& str);
+
+private:
+    PreviewEngine* parent_;
+};
+
+class PreviewEngine : public QWebEngineView
+{
+    Q_OBJECT
+public:
+    explicit PreviewEngine(QObject* parent = nullptr);
+    ~PreviewEngine() = default;
+
+    void parseMessage(const QString& messageId, const QString& msg);
+
+Q_SIGNALS:
+    void infoReady(const QString& messageId, const QVariantMap& info);
+    void linkifyReady(const QString& messageId, const QString& linkified);
+
+private:
+    QWebChannel* channel_;
+    PreviewEnginePrivate* pimpl_;
+};
diff --git a/src/qmlregister.cpp b/src/qmlregister.cpp
index 0b7599beb..31cdc5271 100644
--- a/src/qmlregister.cpp
+++ b/src/qmlregister.cpp
@@ -24,6 +24,7 @@
 #include "contactadapter.h"
 #include "pluginadapter.h"
 #include "messagesadapter.h"
+#include "previewengine.h"
 #include "utilsadapter.h"
 #include "conversationsadapter.h"
 #include "currentconversation.h"
@@ -99,21 +100,22 @@ void
 registerTypes(QQmlEngine* engine,
               SystemTray* systemTray,
               LRCInstance* lrcInstance,
-              AppSettingsManager* appSettingsManager,
+              AppSettingsManager* settingsManager,
+              PreviewEngine* previewEngine,
               ScreenInfo* screenInfo,
               QObject* parent)
 {
     // setup the adapters (their lifetimes are that of MainApplication)
     auto callAdapter = new CallAdapter(systemTray, lrcInstance, parent);
-    auto messagesAdapter = new MessagesAdapter(appSettingsManager, lrcInstance, parent);
+    auto messagesAdapter = new MessagesAdapter(settingsManager, previewEngine, lrcInstance, parent);
     auto conversationsAdapter = new ConversationsAdapter(systemTray, lrcInstance, parent);
     auto avAdapter = new AvAdapter(lrcInstance, parent);
     auto contactAdapter = new ContactAdapter(lrcInstance, parent);
-    auto accountAdapter = new AccountAdapter(appSettingsManager, lrcInstance, parent);
-    auto utilsAdapter = new UtilsAdapter(appSettingsManager, systemTray, lrcInstance, parent);
+    auto accountAdapter = new AccountAdapter(settingsManager, lrcInstance, parent);
+    auto utilsAdapter = new UtilsAdapter(settingsManager, systemTray, lrcInstance, parent);
     auto pluginAdapter = new PluginAdapter(lrcInstance, parent);
     auto currentConversation = new CurrentConversation(lrcInstance, parent);
-    auto currentAccount = new CurrentAccount(lrcInstance, appSettingsManager, parent);
+    auto currentAccount = new CurrentAccount(lrcInstance, settingsManager, parent);
     auto videoDevices = new VideoDevices(lrcInstance, parent);
 
     // qml adapter registration
@@ -155,12 +157,14 @@ registerTypes(QQmlEngine* engine,
     QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
     QML_REGISTERTYPE(NS_MODELS, FilesToSendListModel);
     QML_REGISTERTYPE(NS_MODELS, SmartListModel);
+    QML_REGISTERTYPE(NS_MODELS, MessageListModel);
 
     // Roles & type enums for models
     QML_REGISTERNAMESPACE(NS_MODELS, AccountList::staticMetaObject, "AccountList");
     QML_REGISTERNAMESPACE(NS_MODELS, ConversationList::staticMetaObject, "ConversationList");
     QML_REGISTERNAMESPACE(NS_MODELS, ContactList::staticMetaObject, "ContactList");
     QML_REGISTERNAMESPACE(NS_MODELS, FilesToSend::staticMetaObject, "FilesToSend");
+    QML_REGISTERNAMESPACE(NS_MODELS, MessageList::staticMetaObject, "MessageList");
 
     // QQuickItems
     QML_REGISTERTYPE(NS_MODELS, PreviewRenderer);
@@ -176,10 +180,10 @@ registerTypes(QQmlEngine* engine,
 
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, screenInfo, "ScreenInfo")
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, lrcInstance, "LRCInstance")
-    QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, appSettingsManager, "AppSettingsManager")
+    QML_REGISTERSINGLETONTYPE_POBJECT(NS_CONSTANTS, settingsManager, "AppSettingsManager")
 
     auto avatarRegistry = new AvatarRegistry(lrcInstance, parent);
-    auto wizardViewStepModel = new WizardViewStepModel(lrcInstance, accountAdapter, appSettingsManager, parent);
+    auto wizardViewStepModel = new WizardViewStepModel(lrcInstance, accountAdapter, settingsManager, parent);
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_HELPERS, avatarRegistry, "AvatarRegistry");
     QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, wizardViewStepModel, "WizardViewStepModel")
 
diff --git a/src/qmlregister.h b/src/qmlregister.h
index 54f37b0ec..721de8716 100644
--- a/src/qmlregister.h
+++ b/src/qmlregister.h
@@ -33,6 +33,7 @@
 class SystemTray;
 class LRCInstance;
 class AppSettingsManager;
+class PreviewEngine;
 class ScreenInfo;
 
 // Hack for QtCreator autocomplete (part 1)
@@ -63,6 +64,7 @@ void registerTypes(QQmlEngine* engine,
                    SystemTray* systemTray,
                    LRCInstance* lrcInstance,
                    AppSettingsManager* appSettingsManager,
+                   PreviewEngine* previewEngine,
                    ScreenInfo* screenInfo,
                    QObject* parent);
 }
diff --git a/src/utils.cpp b/src/utils.cpp
index 880a0c562..a903569b9 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -789,7 +789,7 @@ Utils::QByteArrayFromFile(const QString& filename)
     if (file.open(QIODevice::ReadOnly)) {
         return file.readAll();
     } else {
-        qDebug() << "can't open file";
+        qDebug() << "QByteArrayFromFile: can't open file";
         return QByteArray();
     }
 }
diff --git a/src/webchathelpers.cpp b/src/webchathelpers.cpp
deleted file mode 100644
index d265461a8..000000000
--- a/src/webchathelpers.cpp
+++ /dev/null
@@ -1,159 +0,0 @@
-/*
- * Copyright (C) 2017-2020 by Savoir-faire Linux
- * Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>
- * Author: S�bastien Blin <sebastien.blin@savoirfairelinux.com>
- * Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
- * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include "webchathelpers.h"
-
-QJsonObject
-buildInteractionJson(lrc::api::ConversationModel& conversationModel,
-                     const QString& convId,
-                     const QString msgId,
-                     const lrc::api::interaction::Info& inter)
-{
-    QRegExp reg(".(jpeg|jpg|gif|png)$");
-    auto interaction = inter;
-    if (interaction.type == lrc::api::interaction::Type::DATA_TRANSFER) {
-        if (interaction.body.isEmpty())
-            return {};
-        else if (interaction.body.toLower().contains(reg))
-            interaction.body = "file://" + interaction.body;
-    }
-
-    if (interaction.type == lrc::api::interaction::Type::MERGE)
-        return {};
-
-    auto sender = interaction.authorUri;
-    auto timestamp = QString::number(interaction.timestamp);
-    auto direction = lrc::api::interaction::isOutgoing(interaction) ? QString("out")
-                                                                    : QString("in");
-
-    QJsonObject interactionObject = QJsonObject();
-    interactionObject.insert("text", QJsonValue(interaction.body));
-    interactionObject.insert("id", QJsonValue(msgId));
-    interactionObject.insert("sender", QJsonValue(sender));
-    interactionObject.insert("sender_contact_method", QJsonValue(sender));
-    interactionObject.insert("timestamp", QJsonValue(timestamp));
-    interactionObject.insert("direction", QJsonValue(direction));
-    interactionObject.insert("duration", QJsonValue(static_cast<int>(interaction.duration)));
-
-    switch (interaction.type) {
-    case lrc::api::interaction::Type::TEXT:
-        interactionObject.insert("type", QJsonValue("text"));
-        break;
-    case lrc::api::interaction::Type::CALL:
-        interactionObject.insert("type", QJsonValue("call"));
-        break;
-    case lrc::api::interaction::Type::CONTACT:
-        interactionObject.insert("type", QJsonValue("contact"));
-        break;
-    case lrc::api::interaction::Type::DATA_TRANSFER: {
-        interactionObject.insert("type", QJsonValue("data_transfer"));
-        lrc::api::datatransfer::Info info = {};
-        conversationModel.getTransferInfo(convId, msgId, info);
-        if (info.status != lrc::api::datatransfer::Status::INVALID) {
-            interactionObject.insert("totalSize", QJsonValue(qint64(info.totalSize)));
-            interactionObject.insert("progress", QJsonValue(qint64(info.progress)));
-        }
-        interactionObject.insert("displayName", QJsonValue(inter.commit["displayName"]));
-        break;
-    }
-    case lrc::api::interaction::Type::INVALID:
-    default:
-        return {};
-    }
-
-    if (interaction.isRead) {
-        interactionObject.insert("delivery_status", QJsonValue("read"));
-    }
-
-    switch (interaction.status) {
-    case lrc::api::interaction::Status::SUCCESS:
-        interactionObject.insert("delivery_status", QJsonValue("sent"));
-        break;
-    case lrc::api::interaction::Status::FAILURE:
-    case lrc::api::interaction::Status::TRANSFER_ERROR:
-        interactionObject.insert("delivery_status", QJsonValue("failure"));
-        break;
-    case lrc::api::interaction::Status::TRANSFER_UNJOINABLE_PEER:
-        interactionObject.insert("delivery_status", QJsonValue("unjoinable peer"));
-        break;
-    case lrc::api::interaction::Status::SENDING:
-        interactionObject.insert("delivery_status", QJsonValue("sending"));
-        break;
-    case lrc::api::interaction::Status::TRANSFER_CREATED:
-        interactionObject.insert("delivery_status", QJsonValue("connecting"));
-        break;
-    case lrc::api::interaction::Status::TRANSFER_ACCEPTED:
-        interactionObject.insert("delivery_status", QJsonValue("accepted"));
-        break;
-    case lrc::api::interaction::Status::TRANSFER_CANCELED:
-        interactionObject.insert("delivery_status", QJsonValue("canceled"));
-        break;
-    case lrc::api::interaction::Status::TRANSFER_ONGOING:
-        interactionObject.insert("delivery_status", QJsonValue("ongoing"));
-        break;
-    case lrc::api::interaction::Status::TRANSFER_AWAITING_PEER:
-        interactionObject.insert("delivery_status", QJsonValue("awaiting peer"));
-        break;
-    case lrc::api::interaction::Status::TRANSFER_AWAITING_HOST:
-        interactionObject.insert("delivery_status", QJsonValue("awaiting host"));
-        break;
-    case lrc::api::interaction::Status::TRANSFER_TIMEOUT_EXPIRED:
-        interactionObject.insert("delivery_status", QJsonValue("awaiting peer timeout"));
-        break;
-    case lrc::api::interaction::Status::TRANSFER_FINISHED:
-        interactionObject.insert("delivery_status", QJsonValue("finished"));
-        break;
-    case lrc::api::interaction::Status::INVALID:
-    case lrc::api::interaction::Status::UNKNOWN:
-    default:
-        interactionObject.insert("delivery_status", QJsonValue("unknown"));
-        break;
-    }
-    return interactionObject;
-}
-
-QString
-interactionToJsonInteractionObject(lrc::api::ConversationModel& conversationModel,
-                                   const QString& convId,
-                                   const QString& msgId,
-                                   const lrc::api::interaction::Info& interaction)
-{
-    auto interactionObject = buildInteractionJson(conversationModel, convId, msgId, interaction);
-    return QString(QJsonDocument(interactionObject).toJson(QJsonDocument::Compact));
-}
-
-QString
-interactionsToJsonArrayObject(lrc::api::ConversationModel& conversationModel,
-                              const QString& convId,
-                              MessagesList interactions)
-{
-    QJsonArray array;
-    for (const auto& interaction : interactions) {
-        auto interactionObject = buildInteractionJson(conversationModel,
-                                                      convId,
-                                                      interaction.first,
-                                                      interaction.second);
-        if (!interactionObject.isEmpty()) {
-            array.append(interactionObject);
-        }
-    }
-    return QString(QJsonDocument(array).toJson(QJsonDocument::Compact));
-}
diff --git a/src/webchathelpers.h b/src/webchathelpers.h
deleted file mode 100644
index 88fc13085..000000000
--- a/src/webchathelpers.h
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2017-2020 by Savoir-faire Linux
- * Author: Alexandre Viau <alexandre.viau@savoirfairelinux.com>
- * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
- * Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com>
- * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <QFile>
-#include <QJsonArray>
-#include <QJsonDocument>
-#include <QJsonObject>
-
-#include "lrcinstance.h"
-#include "api/conversationmodel.h"
-
-QJsonObject buildInteractionJson(lrc::api::ConversationModel& conversationModel,
-                                 const QString& convId,
-                                 const QString& msgId,
-                                 lrc::api::interaction::Info& interaction);
-QString interactionToJsonInteractionObject(lrc::api::ConversationModel& conversationModel,
-                                           const QString& convId,
-                                           const QString& msgId,
-                                           const lrc::api::interaction::Info& interaction);
-QString interactionsToJsonArrayObject(lrc::api::ConversationModel& conversationModel,
-                                      const QString& convId,
-                                      MessagesList interactions);
diff --git a/tests/qml/main.cpp b/tests/qml/main.cpp
index 86ad4856a..c6d486242 100644
--- a/tests/qml/main.cpp
+++ b/tests/qml/main.cpp
@@ -22,6 +22,7 @@
 #include "appsettingsmanager.h"
 #include "connectivitymonitor.h"
 #include "systemtray.h"
+#include "previewengine.h"
 
 #include <atomic>
 
@@ -75,6 +76,7 @@ public:
                              systemTray_.get(),
                              lrcInstance_.get(),
                              settingsManager_.get(),
+                             previewEngine_.get(),
                              &screenInfo_,
                              this);
     }
@@ -116,6 +118,7 @@ private:
     QScopedPointer<ConnectivityMonitor> connectivityMonitor_;
     QScopedPointer<AppSettingsManager> settingsManager_;
     QScopedPointer<SystemTray> systemTray_;
+    QScopedPointer<PreviewEngine> previewEngine_;
     ScreenInfo screenInfo_;
 
     bool muteDring_ {false};
diff --git a/tests/qml/resources.qrc b/tests/qml/resources.qrc
index ea2093b3f..9dd965abc 100644
--- a/tests/qml/resources.qrc
+++ b/tests/qml/resources.qrc
@@ -2,7 +2,7 @@
     <qresource prefix="/">
         <file>src/tst_LocalAccount.qml</file>
         <file>src/tst_PresenceIndicator.qml</file>
-        <file>src/tst_MessageWebViewFooter.qml</file>
+        <file>src/tst_ChatViewFooter.qml</file>
         <file>src/resources/gif_test.gif</file>
         <file>src/resources/gz_test.gz</file>
         <file>src/resources/png_test.png</file>
diff --git a/tests/qml/src/tst_MessageWebViewFooter.qml b/tests/qml/src/tst_ChatViewFooter.qml
similarity index 97%
rename from tests/qml/src/tst_MessageWebViewFooter.qml
rename to tests/qml/src/tst_ChatViewFooter.qml
index 7d9b2c5e6..f4a7cc8f4 100644
--- a/tests/qml/src/tst_MessageWebViewFooter.qml
+++ b/tests/qml/src/tst_ChatViewFooter.qml
@@ -35,13 +35,13 @@ ColumnLayout {
     width: 300
     height: uut.implicitHeight
 
-    MessageWebViewFooter {
+    ChatViewFooter {
         id: uut
 
         Layout.alignment: Qt.AlignHCenter
         Layout.fillWidth: true
         Layout.preferredHeight: implicitHeight
-        Layout.maximumHeight: JamiTheme.messageWebViewFooterMaximumHeight
+        Layout.maximumHeight: JamiTheme.chatViewMaximumWidth
 
         TestCase {
             name: "MessageWebViewFooter Send Message Button Visibility Test"
diff --git a/tests/qml/src/tst_FilesToSendContainer.qml b/tests/qml/src/tst_FilesToSendContainer.qml
index cdb845ca2..42f42d400 100644
--- a/tests/qml/src/tst_FilesToSendContainer.qml
+++ b/tests/qml/src/tst_FilesToSendContainer.qml
@@ -40,9 +40,9 @@ ColumnLayout {
 
         Layout.alignment: Qt.AlignHCenter
         Layout.preferredWidth: root.width
-        Layout.maximumWidth: JamiTheme.messageWebViewFooterContentMaximumWidth
+        Layout.maximumWidth: JamiTheme.chatViewMaximumWidth
         Layout.preferredHeight: filesToSendCount ?
-                                    JamiTheme.messageWebViewFooterFileContainerPreferredHeight : 0
+                                    JamiTheme.chatViewFooterFileContainerPreferredHeight : 0
 
         TestCase {
             name: "FilesToSendContainer add/remove file test"
-- 
GitLab