diff --git a/qml.qrc b/qml.qrc
index 67fb8da2c23a35c58b3e00eb0f5b225c268c0189..71bf3277b79781cfa71869403fbf08717092adfc 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -161,10 +161,12 @@
         <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/commoncomponents/TextMessageDelegate.qml</file>
         <file>src/mainview/components/MessageListView.qml</file>
         <file>src/commoncomponents/MessageBubble.qml</file>
         <file>src/constant/MsgSeq.qml</file>
         <file>src/commoncomponents/SBSMessageBase.qml</file>
+        <file>src/commoncomponents/GeneratedMessageDelegate.qml</file>
+        <file>src/commoncomponents/DataTransferMessageDelegate.qml</file>
     </qresource>
 </RCC>
diff --git a/src/commoncomponents/MessageDelegate.qml b/src/commoncomponents/DataTransferMessageDelegate.qml
similarity index 57%
rename from src/commoncomponents/MessageDelegate.qml
rename to src/commoncomponents/DataTransferMessageDelegate.qml
index 210530e9531397ff9ed3a25a77e9934477a8750c..287aab3634d1c847a8aa25a3b193f12f1d9ba898 100644
--- a/src/commoncomponents/MessageDelegate.qml
+++ b/src/commoncomponents/DataTransferMessageDelegate.qml
@@ -1,7 +1,8 @@
-/*
+/*
  * Copyright (C) 2021 by Savoir-faire Linux
  * Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
  * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ * 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
@@ -24,272 +25,48 @@ import QtGraphicalEffects 1.15
 import QtWebEngine 1.10
 
 import net.jami.Models 1.1
-import net.jami.Adapters 1.1
 import net.jami.Constants 1.1
+import net.jami.Adapters 1.1
 
-Control {
+Loader {
     id: root
 
-    readonly property ListView listView: ListView.view
-
-    readonly property bool isGenerated: Type === Interaction.Type.CALL ||
-                                        Type === Interaction.Type.CONTACT
-    readonly property string author: Author
-    readonly property var body: Body
-    readonly property var timestamp: Timestamp
-    readonly property bool isOutgoing: model.Author === ""
-    readonly property var formattedTime: MessagesAdapter.getFormattedTime(Timestamp)
-    readonly property var linkInfo: LinkPreviewInfo
     property var mediaInfo
-
-    readonly property real senderMargin: 64
-    readonly property real avatarSize: 32
-    readonly property real msgRadius: 18
-    readonly property real hMargin: 12
-
     property bool showTime: false
     property int seq: MsgSeq.single
 
-    width: parent ? parent.width : 0
-    height: loader.height
-
-    // message interaction
-    property string hoveredLink
-    MouseArea {
-        id: itemMouseArea
-        anchors.fill: parent
-        acceptedButtons: Qt.LeftButton
-        onClicked: {
-            if (root.hoveredLink)
-                Qt.openUrlExternally(root.hoveredLink)
-        }
-    }
-
-    Loader {
-        id: loader
-
-        width: root.width
-        height: sourceComponent.height
+    width: ListView.view ? ListView.view.width : 0
 
-        sourceComponent: {
-            switch (Type) {
-            case Interaction.Type.TEXT: return textMsgComp
-            case Interaction.Type.CALL:
-            case Interaction.Type.CONTACT: return generatedMsgComp
-            case Interaction.Type.DATA_TRANSFER:
-                if (Status === Interaction.Status.TRANSFER_FINISHED) {
-                    mediaInfo = MessagesAdapter.getMediaInfo(Body)
-                    if (Object.keys(mediaInfo).length !== 0)
-                        return localMediaMsgComp
-                }
-                return dataTransferMsgComp
-            default:
-                // if this happens, adjust FilteredMsgListModel
-                console.warn("Invalid message type has not been filtered.")
-                return null
-            }
+    sourceComponent: {
+        if (Status === Interaction.Status.TRANSFER_FINISHED) {
+            mediaInfo = MessagesAdapter.getMediaInfo(Body)
+            if (Object.keys(mediaInfo).length !== 0)
+                return localMediaMsgComp
         }
+        return dataTransferMsgComp
     }
 
-    Component {
-        id: textMsgComp
-
-        SBSMessageBase {
-            property real maxMsgWidth: root.width - senderMargin - 2 * hMargin - avatarBlockWidth
-            property bool isRemoteImage
-            isOutgoing: root.isOutgoing
-            showTime: root.showTime
-            seq: root.seq
-            author: root.author
-            formattedTime: root.formattedTime
-            extraHeight: extraContent.active && !isRemoteImage ? msgRadius : -isRemoteImage
-            innerContent.children: [
-                TextEdit {
-                    padding: 10
-                    anchors.right: isOutgoing ? parent.right : undefined
-                    text: '<span style="white-space: pre-wrap">' + body + '</span>'
-                    width: {
-                        if (extraContent.active)
-                            Math.max(extraContent.width,
-                                     Math.min(implicitWidth - avatarBlockWidth,
-                                              extraContent.minSize) - senderMargin)
-                        else
-                            Math.min(implicitWidth, innerContent.width - senderMargin)
-                    }
-                    height: implicitHeight
-                    wrapMode: Label.WrapAtWordBoundaryOrAnywhere
-                    selectByMouse: true
-                    font.pointSize: 11
-                    font.hintingPreference: Font.PreferNoHinting
-                    renderType: Text.NativeRendering
-                    textFormat: TextEdit.RichText
-                    onLinkHovered: root.hoveredLink = hoveredLink
-                    onLinkActivated: Qt.openUrlExternally(hoveredLink)
-                    readOnly: true
-                    color: isOutgoing ?
-                               JamiTheme.messageOutTxtColor :
-                               JamiTheme.messageInTxtColor
-                },
-                Loader {
-                    id: extraContent
-                    width: sourceComponent.width
-                    height: sourceComponent.height
-                    anchors.right: isOutgoing ? parent.right : undefined
-                    property real minSize: 192
-                    property real maxSize: 320
-                    active: linkInfo.url !== undefined
-                    sourceComponent: ColumnLayout {
-                        id: previewContent
-                        spacing: 12
-                        Component.onCompleted: {
-                            isRemoteImage = MessagesAdapter.isRemoteImage(linkInfo.url)
-                        }
-                        HoverHandler {
-                            target: previewContent
-                            onHoveredChanged: {
-                                root.hoveredLink = hovered ? linkInfo.url : ""
-                            }
-                            cursorShape: Qt.PointingHandCursor
-                        }
-                        AnimatedImage {
-                            id: img
-                            cache: true
-                            source: isRemoteImage ?
-                                        linkInfo.url :
-                                        (hasImage ? linkInfo.image : "")
-                            fillMode: Image.PreserveAspectCrop
-                            mipmap: true
-                            antialiasing: true
-                            autoTransform: true
-                            asynchronous: true
-                            readonly property bool hasImage: linkInfo.image !== null
-                            property real aspectRatio: implicitWidth / implicitHeight
-                            property real adjustedWidth: Math.min(extraContent.maxSize,
-                                                                  Math.max(extraContent.minSize,
-                                                                           maxMsgWidth))
-                            Layout.preferredWidth: adjustedWidth
-                            Layout.preferredHeight: Math.ceil(adjustedWidth / aspectRatio)
-                            Rectangle {
-                                color: JamiTheme.previewImageBackgroundColor
-                                z: -1
-                                anchors.fill: parent
-                            }
-                            layer.enabled: isRemoteImage
-                            layer.effect: OpacityMask {
-                                maskSource: MessageBubble {
-                                    Rectangle { height: msgRadius; width: parent.width }
-                                    out: isOutgoing
-                                    type: seq
-                                    width: img.width
-                                    height: img.height
-                                    radius: msgRadius
-                                }
-                            }
-                        }
-                        Column {
-                            opacity: img.status !== Image.Loading
-                            visible: !isRemoteImage
-                            Layout.preferredWidth: img.width - 2 * hMargin
-                            Layout.leftMargin: hMargin
-                            Layout.rightMargin: hMargin
-                            spacing: 6
-                            Label {
-                                width: parent.width
-                                font.pointSize: 10
-                                font.hintingPreference: Font.PreferNoHinting
-                                wrapMode: Label.WrapAtWordBoundaryOrAnywhere
-                                renderType: Text.NativeRendering
-                                textFormat: TextEdit.RichText
-                                color: JamiTheme.previewTitleColor
-                                visible: linkInfo.title !== null
-                                text: linkInfo.title
-                            }
-                            Label {
-                                width: parent.width
-                                font.pointSize: 11
-                                font.hintingPreference: Font.PreferNoHinting
-                                wrapMode: Label.WrapAtWordBoundaryOrAnywhere
-                                renderType: Text.NativeRendering
-                                textFormat: TextEdit.RichText
-                                color: JamiTheme.previewSubtitleColor
-                                visible: linkInfo.description !== null
-                                text: '<a href=" " style="text-decoration: ' +
-                                      ( hoveredLink ? 'underline' : 'none') + ';"' +
-                                      '>' + linkInfo.description + '</a>'
-                            }
-                            Label {
-                                width: parent.width
-                                font.pointSize: 10
-                                font.hintingPreference: Font.PreferNoHinting
-                                wrapMode: Label.WrapAtWordBoundaryOrAnywhere
-                                renderType: Text.NativeRendering
-                                textFormat: TextEdit.RichText
-                                color: JamiTheme.previewSubtitleColor
-                                text: linkInfo.domain
-                            }
-                        }
-                    }
-                }
-            ]
-            Component.onCompleted: {
-                if (!Linkified) {
-                    MessagesAdapter.parseMessageUrls(Id, Body)
-                }
-            }
-        }
-    }
-
-    Component {
-        id: generatedMsgComp
-
-        Column {
-            width: root.width
-            spacing: 2
-            topPadding: 12
-            bottomPadding: 12
-
-            Label {
-                width: parent.width
-                text: body
-                horizontalAlignment: Qt.AlignHCenter
-                font.pointSize: 12
-                color: JamiTheme.chatviewTextColor
-            }
-
-            Item {
-                id: infoCell
-
-                width: parent.width
-                height: childrenRect.height
-
-                Label {
-                    text: formattedTime
-                    color: JamiTheme.timestampColor
-                    visible: showTime || seq === MsgSeq.last
-                    height: visible * implicitHeight
-                    font.pointSize: 9
-
-                    anchors.horizontalCenter: parent.horizontalCenter
-                }
-            }
-        }
-    }
+    opacity: 0
+    Behavior on opacity { NumberAnimation { duration: 100 } }
+    onLoaded: opacity = 1
 
     Component {
         id: dataTransferMsgComp
 
         SBSMessageBase {
             id: dataTransferItem
+
             property var transferStats: MessagesAdapter.getTransferStats(Id, Status)
             property bool canOpen: Status === Interaction.Status.TRANSFER_FINISHED || isOutgoing
             property real maxMsgWidth: root.width - senderMargin -
-                                       2 * hMargin - avatarBlockWidth
+                                       2 * hPadding - avatarBlockWidth
                                        - buttonsLoader.width - 24 - 6 - 24
-            isOutgoing: root.isOutgoing
+
+            isOutgoing: Author === ""
             showTime: root.showTime
             seq: root.seq
-            author: root.author
-            formattedTime: root.formattedTime
+            author: Author
+            formattedTime: MessagesAdapter.getFormattedTime(Timestamp)
             extraHeight: progressBar.visible ? 18 : 0
             innerContent.children: [
                 RowLayout {
@@ -300,9 +77,8 @@ Control {
                         target: parent
                         enabled: canOpen
                         onHoveredChanged: {
-                            root.hoveredLink = enabled && hovered ?
-                                        ("file:///" + body) :
-                                        ""
+                            dataTransferItem.hoveredLink = enabled && hovered ?
+                                        ("file:///" + Body) : ""
                         }
                         cursorShape: enabled ?
                                          Qt.PointingHandCursor :
@@ -393,7 +169,7 @@ Control {
                             topPadding: 10
                             text: CurrentConversation.isSwarm ?
                                       TransferName :
-                                      body
+                                      Body
                             wrapMode: Label.WrapAtWordBoundaryOrAnywhere
                             font.weight: Font.DemiBold
                             font.pointSize: 11
@@ -404,10 +180,10 @@ Control {
                                        JamiTheme.messageInTxtColor
                             MouseArea {
                                 anchors.fill: parent
+                                propagateComposedEvents: true
                                 cursorShape: canOpen ?
                                                  Qt.PointingHandCursor :
                                                  Qt.ArrowCursor
-                                onClicked: if(canOpen) itemMouseArea.clicked(mouse)
                             }
                         }
                         Label {
@@ -450,11 +226,13 @@ Control {
         id: localMediaMsgComp
 
         SBSMessageBase {
-            isOutgoing: root.isOutgoing
+            id: localMediaMsgItem
+
+            isOutgoing: Author === ""
             showTime: root.showTime
             seq: root.seq
-            author: root.author
-            formattedTime: root.formattedTime
+            author: Author
+            formattedTime: MessagesAdapter.getFormattedTime(Timestamp)
             bubble.visible: false
             innerContent.children: [
                 Loader {
@@ -522,7 +300,7 @@ Control {
                             antialiasing: true
                             autoTransform: false
                             asynchronous: true
-                            source: "file:///" + body
+                            source: "file:///" + Body
                             property real aspectRatio: implicitWidth / implicitHeight
                             property real adjustedWidth: Math.min(maxSize,
                                                                   Math.max(minSize,
@@ -547,7 +325,7 @@ Control {
                             HoverHandler {
                                 target : parent
                                 onHoveredChanged: {
-                                    root.hoveredLink = hovered ? img.source : ""
+                                    localMediaMsgItem.hoveredLink = hovered ? img.source : ""
                                 }
                                 cursorShape: Qt.PointingHandCursor
                             }
@@ -557,8 +335,4 @@ Control {
             ]
         }
     }
-
-    opacity: 0
-    Behavior on opacity { NumberAnimation { duration: 40 } }
-    Component.onCompleted: opacity = 1
 }
diff --git a/src/commoncomponents/GeneratedMessageDelegate.qml b/src/commoncomponents/GeneratedMessageDelegate.qml
new file mode 100644
index 0000000000000000000000000000000000000000..b69335c2e5cde759cc4af50d00886b78f3cf1ef1
--- /dev/null
+++ b/src/commoncomponents/GeneratedMessageDelegate.qml
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2021 by Savoir-faire Linux
+ * Author: Trevor Tabah <trevor.tabah@savoirfairelinux.com>
+ * Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ * 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 net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+
+Column {
+    id: root
+
+    property bool showTime: false
+    property int seq: MsgSeq.single
+
+    width: ListView.view ? ListView.view.width : 0
+
+    spacing: 2
+    topPadding: 12
+    bottomPadding: 12
+
+    Label {
+        width: parent.width
+        text: Body
+        horizontalAlignment: Qt.AlignHCenter
+        font.pointSize: 12
+        color: JamiTheme.chatviewTextColor
+    }
+
+    Item {
+        id: infoCell
+
+        width: parent.width
+        height: childrenRect.height
+
+        Label {
+            text: MessagesAdapter.getFormattedTime(Timestamp)
+            color: JamiTheme.timestampColor
+            visible: showTime || seq === MsgSeq.last
+            height: visible * implicitHeight
+            font.pointSize: 9
+
+            anchors.horizontalCenter: parent.horizontalCenter
+        }
+    }
+
+    opacity: 0
+    Behavior on opacity { NumberAnimation { duration: 100 } }
+    Component.onCompleted: opacity = 1
+}
diff --git a/src/commoncomponents/SBSMessageBase.qml b/src/commoncomponents/SBSMessageBase.qml
index 0c9cafa72304023618817fcdb14371198c124c3c..ac2114086e0afdfc66337d81296e241f9ae99592 100644
--- a/src/commoncomponents/SBSMessageBase.qml
+++ b/src/commoncomponents/SBSMessageBase.qml
@@ -25,7 +25,7 @@ import net.jami.Models 1.1
 import net.jami.Adapters 1.1
 import net.jami.Constants 1.1
 
-ColumnLayout {
+Control {
     id: root
 
     property alias avatarBlockWidth: avatarBlock.width
@@ -39,77 +39,99 @@ ColumnLayout {
     property int seq
     property string author
     property string formattedTime
+    property string hoveredLink
 
     readonly property real senderMargin: 64
     readonly property real avatarSize: 32
     readonly property real msgRadius: 18
-    readonly property real hMargin: 12
-
-    anchors.left: parent.left
-    anchors.right: parent.right
-    anchors.leftMargin: hMargin
-    anchors.rightMargin: hMargin
-    spacing: 2
-
-    RowLayout {
-        Layout.preferredHeight: innerContent.height + root.extraHeight
-        Layout.topMargin: (seq === MsgSeq.first || seq === MsgSeq.single) ? 6 : 0
-        spacing: 0
-        Item {
-            id: avatarBlock
-            Layout.preferredWidth: isOutgoing ? 0 : avatar.width + hMargin
-            Layout.preferredHeight: isOutgoing ? 0 : bubble.height
-            Avatar {
-                id: avatar
-                visible: !isOutgoing && (seq === MsgSeq.last || seq === MsgSeq.single)
-                anchors.bottom: parent.bottom
-                width: avatarSize
-                height: avatarSize
-                imageId: author
-                showPresenceIndicator: false
-                mode: Avatar.Mode.Contact
+    readonly property real hPadding: 12
+
+    width: ListView.view ? ListView.view.width : 0
+    height: mainColumnLayout.implicitHeight
+
+    rightPadding: hPadding
+    leftPadding: hPadding
+
+    contentItem: ColumnLayout {
+        id: mainColumnLayout
+
+        anchors.centerIn: parent
+
+        width: parent.width
+
+        spacing: 2
+
+        RowLayout {
+            Layout.preferredHeight: innerContent.height + root.extraHeight
+            Layout.topMargin: (seq === MsgSeq.first || seq === MsgSeq.single) ? 6 : 0
+            spacing: 0
+            Item {
+                id: avatarBlock
+                Layout.preferredWidth: isOutgoing ? 0 : avatar.width + hPadding
+                Layout.preferredHeight: isOutgoing ? 0 : bubble.height
+                Avatar {
+                    id: avatar
+                    visible: !isOutgoing && (seq === MsgSeq.last || seq === MsgSeq.single)
+                    anchors.bottom: parent.bottom
+                    width: avatarSize
+                    height: avatarSize
+                    imageId: author
+                    showPresenceIndicator: false
+                    mode: Avatar.Mode.Contact
+                }
+            }
+            Item {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                Column {
+                    id: innerContent
+                    width: parent.width
+                    // place actual content here
+                }
+                MessageBubble {
+                    id: bubble
+                    z:-1
+                    out: isOutgoing
+                    type: seq
+                    color: isOutgoing ?
+                               JamiTheme.messageOutBgColor :
+                               JamiTheme.messageInBgColor
+                    radius: msgRadius
+                    anchors.right: isOutgoing ? parent.right : undefined
+                    width: innerContent.childrenRect.width
+                    height: innerContent.childrenRect.height + (visible ? root.extraHeight : 0)
+                }
             }
         }
         Item {
+            id: infoCell
+
             Layout.fillWidth: true
-            Layout.fillHeight: true
-            Column {
-                id: innerContent
-                width: parent.width
-                // place actual content here
-            }
-            MessageBubble {
-                id: bubble
-                z:-1
-                out: isOutgoing
-                type: seq
-                color: isOutgoing ?
-                           JamiTheme.messageOutBgColor :
-                           JamiTheme.messageInBgColor
-                radius: msgRadius
-                anchors.right: isOutgoing ? parent.right : undefined
-                width: innerContent.childrenRect.width
-                height: innerContent.childrenRect.height + (visible ? root.extraHeight : 0)
+            Layout.preferredHeight: childrenRect.height
+
+            Label {
+                text: formattedTime
+                color: JamiTheme.timestampColor
+                visible: showTime || seq === MsgSeq.last
+                height: visible * implicitHeight
+                font.pointSize: 9
+
+                anchors.right: !isOutgoing ? undefined : parent.right
+                anchors.rightMargin: 8
+                anchors.left: isOutgoing ? undefined : parent.left
+                anchors.leftMargin: avatarBlockWidth + 6
             }
         }
     }
-    Item {
-        id: infoCell
-
-        Layout.preferredWidth: parent.width
-        Layout.preferredHeight: childrenRect.height
-
-        Label {
-            text: formattedTime
-            color: JamiTheme.timestampColor
-            visible: showTime || seq === MsgSeq.last
-            height: visible * implicitHeight
-            font.pointSize: 9
-
-            anchors.right: !isOutgoing ? undefined : parent.right
-            anchors.rightMargin: 8
-            anchors.left: isOutgoing ? undefined : parent.left
-            anchors.leftMargin: avatarBlockWidth + 6
+
+    MouseArea {
+        id: itemMouseArea
+        anchors.fill: parent
+        z: -1
+        acceptedButtons: Qt.LeftButton
+        onClicked: {
+            if (root.hoveredLink)
+                Qt.openUrlExternally(root.hoveredLink)
         }
     }
 }
diff --git a/src/commoncomponents/TextMessageDelegate.qml b/src/commoncomponents/TextMessageDelegate.qml
new file mode 100644
index 0000000000000000000000000000000000000000..61b717023b7aaa611256185bab50a5755a215120
--- /dev/null
+++ b/src/commoncomponents/TextMessageDelegate.qml
@@ -0,0 +1,177 @@
+/*
+ * 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 QtGraphicalEffects 1.15
+
+import net.jami.Models 1.1
+import net.jami.Adapters 1.1
+import net.jami.Constants 1.1
+
+SBSMessageBase {
+    id : root
+
+    property bool isRemoteImage
+    property real maxMsgWidth: root.width - senderMargin - 2 * hPadding - avatarBlockWidth
+
+    isOutgoing: Author === ""
+    author: Author
+    formattedTime: MessagesAdapter.getFormattedTime(Timestamp)
+    extraHeight: extraContent.active && !isRemoteImage ? msgRadius : -isRemoteImage
+    innerContent.children: [
+        TextEdit {
+            padding: 10
+            anchors.right: isOutgoing ? parent.right : undefined
+            text: '<span style="white-space: pre-wrap">' + Body + '</span>'
+            width: {
+                if (extraContent.active)
+                    Math.max(extraContent.width,
+                             Math.min(implicitWidth - avatarBlockWidth,
+                                      extraContent.minSize) - senderMargin)
+                else
+                    Math.min(implicitWidth, innerContent.width - senderMargin)
+            }
+            height: implicitHeight
+            wrapMode: Label.WrapAtWordBoundaryOrAnywhere
+            selectByMouse: true
+            font.pointSize: 11
+            font.hintingPreference: Font.PreferNoHinting
+            renderType: Text.NativeRendering
+            textFormat: TextEdit.RichText
+            onLinkHovered: root.hoveredLink = hoveredLink
+            onLinkActivated: Qt.openUrlExternally(hoveredLink)
+            readOnly: true
+            color: isOutgoing ?
+                       JamiTheme.messageOutTxtColor :
+                       JamiTheme.messageInTxtColor
+        },
+        Loader {
+            id: extraContent
+            width: sourceComponent.width
+            height: sourceComponent.height
+            anchors.right: isOutgoing ? parent.right : undefined
+            property real minSize: 192
+            property real maxSize: 320
+            active: LinkPreviewInfo.url !== undefined
+            sourceComponent: ColumnLayout {
+                id: previewContent
+                spacing: 12
+                Component.onCompleted: {
+                    isRemoteImage = MessagesAdapter.isRemoteImage(LinkPreviewInfo.url)
+                }
+                HoverHandler {
+                    target: previewContent
+                    onHoveredChanged: {
+                        root.hoveredLink = hovered ? LinkPreviewInfo.url : ""
+                    }
+                    cursorShape: Qt.PointingHandCursor
+                }
+                AnimatedImage {
+                    id: img
+                    cache: true
+                    source: isRemoteImage ?
+                                LinkPreviewInfo.url :
+                                (hasImage ? LinkPreviewInfo.image : "")
+
+                    fillMode: Image.PreserveAspectCrop
+                    mipmap: true
+                    antialiasing: true
+                    autoTransform: true
+                    asynchronous: true
+                    readonly property bool hasImage: LinkPreviewInfo.image !== null
+                    property real aspectRatio: implicitWidth / implicitHeight
+                    property real adjustedWidth: Math.min(extraContent.maxSize,
+                                                          Math.max(extraContent.minSize,
+                                                                   maxMsgWidth))
+                    Layout.preferredWidth: adjustedWidth
+                    Layout.preferredHeight: Math.ceil(adjustedWidth / aspectRatio)
+                    Rectangle {
+                        color: JamiTheme.previewImageBackgroundColor
+                        z: -1
+                        anchors.fill: parent
+                    }
+                    layer.enabled: isRemoteImage
+                    layer.effect: OpacityMask {
+                        maskSource: MessageBubble {
+                            Rectangle { height: msgRadius; width: parent.width }
+                            out: isOutgoing
+                            type: seq
+                            width: img.width
+                            height: img.height
+                            radius: msgRadius
+                        }
+                    }
+                }
+                Column {
+                    opacity: img.status !== Image.Loading
+                    visible: !isRemoteImage
+                    Layout.preferredWidth: img.width - 2 * hPadding
+                    Layout.leftMargin: hPadding
+                    Layout.rightMargin: hPadding
+                    spacing: 6
+                    Label {
+                        width: parent.width
+                        font.pointSize: 10
+                        font.hintingPreference: Font.PreferNoHinting
+                        wrapMode: Label.WrapAtWordBoundaryOrAnywhere
+                        renderType: Text.NativeRendering
+                        textFormat: TextEdit.RichText
+                        color: JamiTheme.previewTitleColor
+                        visible: LinkPreviewInfo.title !== null
+                        text: LinkPreviewInfo.title
+                    }
+                    Label {
+                        width: parent.width
+                        font.pointSize: 11
+                        font.hintingPreference: Font.PreferNoHinting
+                        wrapMode: Label.WrapAtWordBoundaryOrAnywhere
+                        renderType: Text.NativeRendering
+                        textFormat: TextEdit.RichText
+                        color: JamiTheme.previewSubtitleColor
+                        visible: LinkPreviewInfo.description !== null
+                        text: '<a href=" " style="text-decoration: ' +
+                              ( hoveredLink ? 'underline' : 'none') + ';"' +
+                              '>' + LinkPreviewInfo.description + '</a>'
+                    }
+                    Label {
+                        width: parent.width
+                        font.pointSize: 10
+                        font.hintingPreference: Font.PreferNoHinting
+                        wrapMode: Label.WrapAtWordBoundaryOrAnywhere
+                        renderType: Text.NativeRendering
+                        textFormat: TextEdit.RichText
+                        color: JamiTheme.previewSubtitleColor
+                        text: LinkPreviewInfo.domain
+                    }
+                }
+            }
+        }
+    ]
+
+    opacity: 0
+    Behavior on opacity { NumberAnimation { duration: 100 } }
+    Component.onCompleted: {
+        if (!Linkified) {
+            MessagesAdapter.parseMessageUrls(Id, Body)
+        }
+        opacity = 1
+    }
+}
diff --git a/src/mainview/components/MessageListView.qml b/src/mainview/components/MessageListView.qml
index 2224edc14948209d1024e2e220930aabd2c25f4c..a320174b71cef32b9f94d61b03942d08ffd5b67b 100644
--- a/src/mainview/components/MessageListView.qml
+++ b/src/mainview/components/MessageListView.qml
@@ -20,6 +20,7 @@
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.15
+import Qt.labs.qmlmodels 1.0
 
 import net.jami.Models 1.1
 import net.jami.Adapters 1.1
@@ -30,6 +31,118 @@ import "../../commoncomponents"
 ListView {
     id: root
 
+    function getDistanceToBottom() {
+        const scrollDiff = ScrollBar.vertical.position -
+                         (1.0 - ScrollBar.vertical.size)
+        return Math.abs(scrollDiff) * contentHeight
+    }
+
+    function loadMoreMsgsIfNeeded() {
+        if (atYBeginning && !CurrentConversation.allMessagesLoaded)
+            MessagesAdapter.loadMoreMessages()
+    }
+
+    // sequencing/timestamps (2-sided style)
+    function computeTimestampVisibility(item, itemIndex) {
+        if (root === undefined)
+            return
+        var nItem = root.itemAtIndex(itemIndex - 1)
+        if (nItem && itemIndex !== root.count - 1) {
+            item.showTime = (nItem.timestamp - item.timestamp) > 60 &&
+                    nItem.formattedTime !== item.formattedTime
+        } else {
+            item.showTime = true
+            var pItem = root.itemAtIndex(itemIndex + 1)
+            if (pItem) {
+                pItem.showTime = (item.timestamp - pItem.timestamp) > 60 &&
+                        pItem.formattedTime !== item.formattedTime
+            }
+        }
+    }
+
+    function computeSequencing(computeItem, computeItemIndex) {
+        if (root === undefined)
+            return
+        var cItem = {
+            'author': computeItem.author,
+            'showTime': computeItem.showTime
+        }
+        var pItem = root.itemAtIndex(computeItemIndex + 1)
+        var nItem = root.itemAtIndex(computeItemIndex - 1)
+
+        let isSeq = (item0, item1) =>
+            item0.author === item1.author && !item0.showTime
+
+        let setSeq = function (newSeq, item) {
+            if (item === undefined)
+                computeItem.seq = newSeq
+            else
+                item.seq = newSeq
+        }
+
+        let rAdjustSeq = function (item) {
+            if (item.seq === MsgSeq.last)
+                item.seq = MsgSeq.middle
+            else if (item.seq === MsgSeq.single)
+                setSeq(MsgSeq.first, item)
+        }
+
+        let adjustSeq = function (item) {
+            if (item.seq === MsgSeq.first)
+                item.seq = MsgSeq.middle
+            else if (item.seq === MsgSeq.single)
+                setSeq(MsgSeq.last, item)
+        }
+
+        if (pItem && !nItem) {
+            if (!isSeq(pItem, cItem)) {
+                computeItem.seq = MsgSeq.single
+            } else {
+                computeItem.seq = MsgSeq.last
+                rAdjustSeq(pItem)
+            }
+        } else if (nItem && !pItem) {
+            if (!isSeq(cItem, nItem)) {
+                computeItem.seq = MsgSeq.single
+            } else {
+                setSeq(MsgSeq.first)
+                adjustSeq(nItem)
+            }
+        } else if (!nItem && !pItem) {
+            computeItem.seq = MsgSeq.single
+        } else {
+            if (isSeq(pItem, nItem)) {
+                if (isSeq(pItem, cItem)) {
+                    computeItem.seq = MsgSeq.middle
+                } else {
+                    computeItem.seq = MsgSeq.single
+
+                    if (pItem.seq === MsgSeq.first)
+                        pItem.seq = MsgSeq.single
+                    else if (item.seq === MsgSeq.middle)
+                        pItem.seq = MsgSeq.last
+
+                    if (nItem.seq === MsgSeq.last)
+                        nItem.seq = MsgSeq.single
+                    else if (nItem.seq === MsgSeq.middle)
+                        nItem.seq = MsgSeq.first
+                }
+            } else {
+                if (!isSeq(pItem, cItem)) {
+                    computeItem.seq = MsgSeq.first
+                    adjustSeq(pItem)
+                } else {
+                    computeItem.seq = MsgSeq.last
+                    rAdjustSeq(nItem)
+                }
+            }
+        }
+
+        if (computeItem.seq === MsgSeq.last) {
+            computeItem.showTime = true
+        }
+    }
+
     // fade-in mechanism
     Component.onCompleted: fadeAnimation.start()
     Rectangle {
@@ -74,135 +187,64 @@ ListView {
 
     model: MessagesAdapter.messageListModel
 
-    delegate: MessageDelegate {
-        // sequencing/timestamps (2-sided style)
-        function computeTimestampVisibility() {
-            if (listView === undefined)
-                return
-            var nItem = listView.itemAtIndex(index - 1)
-            if (nItem && index !== listView.count - 1) {
-                showTime = (nItem.timestamp - timestamp) > 60 &&
-                        nItem.formattedTime !== formattedTime
-            } else {
-                showTime = true
-                var pItem = listView.itemAtIndex(index + 1)
-                if (pItem) {
-                    pItem.showTime = (timestamp - pItem.timestamp) > 60 &&
-                            pItem.formattedTime !== formattedTime
+    delegate: DelegateChooser {
+        id: delegateChooser
+
+        role: "Type"
+        DelegateChoice {
+            roleValue: Interaction.Type.TEXT
+            TextMessageDelegate {
+                Component.onCompleted: {
+                    if (index) {
+                        computeTimestampVisibility(this, index)
+                        computeSequencing(this, index)
+                    } else {
+                        Qt.callLater(computeTimestampVisibility, this, index)
+                        Qt.callLater(computeSequencing, this, index)
+                    }
                 }
             }
         }
-
-        function computeSequencing() {
-            if (listView === undefined)
-                return
-            var cItem = {
-                'author': author,
-                'isGenerated': isGenerated,
-                'showTime': showTime
-            }
-            var pItem = listView.itemAtIndex(index + 1)
-            var nItem = listView.itemAtIndex(index - 1)
-
-            let isSeq = (item0, item1) =>
-                item0.author === item1.author &&
-                !(item0.isGenerated || item1.isGenerated) &&
-                !item0.showTime
-
-            let setSeq = function (newSeq, item) {
-                if (item === undefined)
-                    seq = isGenerated ? MsgSeq.single : newSeq
-                else
-                    item.seq = item.isGenerated ? MsgSeq.single : newSeq
-            }
-
-            let rAdjustSeq = function (item) {
-                if (item.seq === MsgSeq.last)
-                    item.seq = MsgSeq.middle
-                else if (item.seq === MsgSeq.single)
-                    setSeq(MsgSeq.first, item)
-            }
-
-            let adjustSeq = function (item) {
-                if (item.seq === MsgSeq.first)
-                    item.seq = MsgSeq.middle
-                else if (item.seq === MsgSeq.single)
-                    setSeq(MsgSeq.last, item)
-            }
-
-            if (pItem && !nItem) {
-                if (!isSeq(pItem, cItem)) {
-                    seq = MsgSeq.single
-                } else {
-                    seq = MsgSeq.last
-                    rAdjustSeq(pItem)
+        DelegateChoice {
+            roleValue: Interaction.Type.CALL
+            GeneratedMessageDelegate {
+                Component.onCompleted: {
+                    if (index)
+                        computeTimestampVisibility(this, index)
+                    else
+                        Qt.callLater(computeTimestampVisibility, this, index)
                 }
-            } else if (nItem && !pItem) {
-                if (!isSeq(cItem, nItem)) {
-                    seq = MsgSeq.single
-                } else {
-                    setSeq(MsgSeq.first)
-                    adjustSeq(nItem)
+            }
+        }
+        DelegateChoice {
+            roleValue: Interaction.Type.CONTACT
+            GeneratedMessageDelegate {
+                Component.onCompleted: {
+                    if (index)
+                        computeTimestampVisibility(this, index)
+                    else
+                        Qt.callLater(computeTimestampVisibility, this, index)
                 }
-            } else if (!nItem && !pItem) {
-                seq = MsgSeq.single
-            } else {
-                if (isSeq(pItem, nItem)) {
-                    if (isSeq(pItem, cItem)) {
-                        seq = MsgSeq.middle
-                    } else {
-                        seq = MsgSeq.single
-
-                        if (pItem.seq === MsgSeq.first)
-                            pItem.seq = MsgSeq.single
-                        else if (item.seq === MsgSeq.middle)
-                            pItem.seq = MsgSeq.last
-
-                        if (nItem.seq === MsgSeq.last)
-                            nItem.seq = MsgSeq.single
-                        else if (nItem.seq === MsgSeq.middle)
-                            nItem.seq = MsgSeq.first
-                    }
-                } else {
-                    if (!isSeq(pItem, cItem)) {
-                        seq = MsgSeq.first
-                        adjustSeq(pItem)
+            }
+        }
+        DelegateChoice {
+            roleValue: Interaction.Type.DATA_TRANSFER
+            DataTransferMessageDelegate {
+                Component.onCompleted: {
+                    if (index) {
+                        computeTimestampVisibility(this, index)
+                        computeSequencing(this, index)
                     } else {
-                        seq = MsgSeq.last
-                        rAdjustSeq(nItem)
+                        Qt.callLater(computeTimestampVisibility, this, index)
+                        Qt.callLater(computeSequencing, this, index)
                     }
                 }
             }
-
-            if (seq === MsgSeq.last) {
-                showTime = true
-            }
-        }
-
-        Component.onCompleted: {
-            if (index) {
-                computeTimestampVisibility()
-                computeSequencing()
-            } else {
-                Qt.callLater(computeTimestampVisibility)
-                Qt.callLater(computeSequencing)
-            }
         }
     }
 
-    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