diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml
index eee3879cefa055c52ab2a9af253f681c077bc16d..127b59c22566b4f3cc3b448865b14fa890c406e9 100644
--- a/resources/qml/ForwardCompleter.qml
+++ b/resources/qml/ForwardCompleter.qml
@@ -50,9 +50,24 @@ Popup {
         Reply {
             id: replyPreview
 
-            modelData: room ? room.getDump(mid, "") : {
+            property var modelData: room ? room.getDump(mid, "") : {
             }
+
             userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window)
+            blurhash: modelData.blurhash ?? ""
+            body: modelData.body ?? ""
+            formattedBody: modelData.formattedBody ?? ""
+            eventId: modelData.eventId ?? ""
+            filename: modelData.filename ?? ""
+            filesize: modelData.filesize ?? ""
+            proportionalHeight: modelData.proportionalHeight ?? 1
+            type: modelData.type ?? MtxEvent.UnknownMessage
+            typeString: modelData.typeString ?? ""
+            url: modelData.url ?? ""
+            originalWidth: modelData.originalWidth ?? 0
+            isOnlyEmoji: modelData.isOnlyEmoji ?? false
+            userId: modelData.userId ?? ""
+            userName: modelData.userName ?? ""
         }
 
         MatrixTextField {
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 260bc9daf50088e842b2f962339a79b1868b9d93..9e01ef9ad75ced1418a93aa9ba4110e2fb4df652 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -7,8 +7,8 @@ import "./emoji"
 import "./ui"
 import Qt.labs.platform 1.1 as Platform
 import QtGraphicalEffects 1.0
-import QtQuick 2.12
-import QtQuick.Controls 2.3
+import QtQuick 2.15
+import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.2
 import QtQuick.Window 2.2
 import im.nheko 1.0
@@ -25,6 +25,9 @@ ScrollView {
         property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
 
         model: room
+        // reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107
+        //onModelChanged: if (room) room.sendReset()
+        //reuseItems: true
         boundsBehavior: Flickable.StopAtBounds
         pixelAligned: true
         spacing: 4
@@ -84,7 +87,7 @@ ScrollView {
                     ToolTip.text: qsTr("Edit")
                     onClicked: {
                         if (row.model.isEditable)
-                            chat.model.editAction(row.model.id);
+                            chat.model.editAction(row.model.eventId);
 
                     }
                 }
@@ -98,7 +101,7 @@ ScrollView {
                     ToolTip.visible: hovered
                     ToolTip.text: qsTr("React")
                     emojiPicker: emojiPopup
-                    event_id: row.model ? row.model.id : ""
+                    event_id: row.model ? row.model.eventId : ""
                 }
 
                 ImageButton {
@@ -110,7 +113,7 @@ ScrollView {
                     image: ":/icons/icons/ui/mail-reply.png"
                     ToolTip.visible: hovered
                     ToolTip.text: qsTr("Reply")
-                    onClicked: chat.model.replyAction(row.model.id)
+                    onClicked: chat.model.replyAction(row.model.eventId)
                 }
 
                 ImageButton {
@@ -121,7 +124,7 @@ ScrollView {
                     image: ":/icons/icons/ui/vertical-ellipsis.png"
                     ToolTip.visible: hovered
                     ToolTip.text: qsTr("Options")
-                    onClicked: messageContextMenu.show(row.model.id, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
+                    onClicked: messageContextMenu.show(row.model.eventId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
                 }
 
             }
@@ -212,16 +215,16 @@ ScrollView {
                 topPadding: 4
                 bottomPadding: 4
                 spacing: 8
-                visible: modelData && (modelData.previousMessageUserId !== modelData.userId || modelData.previousMessageDay !== modelData.day)
+                visible: (previousMessageUserId !== userId || previousMessageDay !== day)
                 width: parentWidth
-                height: ((modelData && modelData.previousMessageDay !== modelData.day) ? dateBubble.height + 8 + userName.height : userName.height) + 8
+                height: ((previousMessageDay !== day) ? dateBubble.height + 8 + userName.height : userName.height) + 8
 
                 Label {
                     id: dateBubble
 
                     anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-                    visible: modelData && modelData.previousMessageDay !== modelData.day
-                    text: modelData ? chat.model.formatDateSeparator(modelData.timestamp) : ""
+                    visible: previousMessageDay !== day
+                    text: chat.model.formatDateSeparator(timestamp)
                     color: Nheko.colors.text
                     height: Math.round(fontMetrics.height * 1.4)
                     width: contentWidth * 1.2
@@ -236,7 +239,7 @@ ScrollView {
                 }
 
                 Row {
-                    height: userName.height
+                    height: userName_.height
                     spacing: 8
 
                     Avatar {
@@ -244,10 +247,10 @@ ScrollView {
 
                         width: Nheko.avatarSize
                         height: Nheko.avatarSize
-                        url: modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : ""
-                        displayName: modelData ? modelData.userName : ""
-                        userid: modelData ? modelData.userId : ""
-                        onClicked: chat.model.openUserProfile(modelData.userId)
+                        url: chat.model.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
+                        displayName: userName
+                        userid: userId
+                        onClicked: chat.model.openUserProfile(userId)
                         ToolTip.visible: avatarHover.hovered
                         ToolTip.text: userid
 
@@ -260,22 +263,22 @@ ScrollView {
                     Connections {
                         target: chat.model
                         onRoomAvatarUrlChanged: {
-                            messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "";
+                            messageUserAvatar.url = chat.model.avatarUrl(userId).replace("mxc://", "image://MxcImage/");
                         }
                         onScrollToIndex: chat.positionViewAtIndex(index, ListView.Visible)
                     }
 
                     Label {
-                        id: userName
+                        id: userName_
 
-                        text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : ""
-                        color: TimelineManager.userColor(modelData ? modelData.userId : "", Nheko.colors.window)
+                        text: TimelineManager.escapeEmoji(userName)
+                        color: TimelineManager.userColor(userId, Nheko.colors.window)
                         textFormat: Text.RichText
                         ToolTip.visible: displayNameHover.hovered
-                        ToolTip.text: modelData ? modelData.userId : ""
+                        ToolTip.text: userId
 
                         TapHandler {
-                            onSingleTapped: chat.model.openUserProfile(modelData.userId)
+                            onSingleTapped: chat.model.openUserProfile(userId)
                         }
 
                         CursorShape {
@@ -291,7 +294,7 @@ ScrollView {
 
                     Label {
                         color: Nheko.colors.buttonText
-                        text: modelData ? TimelineManager.userStatus(modelData.userId) : ""
+                        text: TimelineManager.userStatus(userId)
                         textFormat: Text.PlainText
                         elide: Text.ElideRight
                         width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - Nheko.avatarSize
@@ -307,7 +310,35 @@ ScrollView {
         delegate: Item {
             id: wrapper
 
-            property bool scrolledToThis: model.id === chat.model.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
+            required property double proportionalHeight
+            required property int type
+            required property string typeString
+            required property int originalWidth
+            required property string blurhash
+            required property string body
+            required property string formattedBody
+            required property string eventId
+            required property string filename
+            required property string filesize
+            required property string url
+            required property string thumbnailUrl
+            required property bool isOnlyEmoji
+            required property bool isSender
+            required property bool isEncrypted
+            required property bool isEditable
+            required property bool isEdited
+            required property string replyTo
+            required property string userId
+            required property var reactions
+            required property int trustlevel
+            required property var timestamp
+            required property int status
+            required property int index
+            required property string previousMessageUserId
+            required property string day
+            required property string previousMessageDay
+            required property string userName
+            property bool scrolledToThis: eventId === chat.model.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
 
             anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
             width: chat.delegateMaxWidth
@@ -362,10 +393,15 @@ ScrollView {
             Loader {
                 id: section
 
-                property var modelData: model
                 property int parentWidth: parent.width
-
-                active: model.previousMessageUserId !== undefined && model.previousMessageUserId !== model.userId || model.previousMessageDay !== model.day
+                property string userId: wrapper.userId
+                property string previousMessageUserId: wrapper.previousMessageUserId
+                property string day: wrapper.day
+                property string previousMessageDay: wrapper.previousMessageDay
+                property string userName: wrapper.userName
+                property var timestamp: wrapper.timestamp
+
+                active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day
                 //asynchronous: true
                 sourceComponent: sectionHeader
                 visible: status == Loader.Ready
@@ -376,6 +412,30 @@ ScrollView {
 
                 property alias hovered: hoverHandler.hovered
 
+                proportionalHeight: wrapper.proportionalHeight
+                type: chat.model, wrapper.type
+                typeString: wrapper.typeString
+                originalWidth: wrapper.originalWidth
+                blurhash: wrapper.blurhash
+                body: wrapper.body
+                formattedBody: wrapper.formattedBody
+                eventId: chat.model, wrapper.eventId
+                filename: wrapper.filename
+                filesize: wrapper.filesize
+                url: wrapper.url
+                thumbnailUrl: wrapper.thumbnailUrl
+                isOnlyEmoji: wrapper.isOnlyEmoji
+                isSender: wrapper.isSender
+                isEncrypted: wrapper.isEncrypted
+                isEditable: wrapper.isEditable
+                isEdited: wrapper.isEdited
+                replyTo: wrapper.replyTo
+                userId: wrapper.userId
+                userName: wrapper.userName
+                reactions: wrapper.reactions
+                trustlevel: wrapper.trustlevel
+                timestamp: wrapper.timestamp
+                status: wrapper.status
                 y: section.visible && section.active ? section.y + section.height : 0
 
                 HoverHandler {
@@ -386,7 +446,7 @@ ScrollView {
                         if (hovered) {
                             if (!messageActionHover.hovered) {
                                 messageActions.attached = timelinerow;
-                                messageActions.model = model;
+                                messageActions.model = timelinerow;
                             }
                         }
                     }
diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml
index 0de68fe866df7a4944e32d17f62eefba5a260f3a..54b4f20c480e5e8241194762f9c5d99088033806 100644
--- a/resources/qml/ReplyPopup.qml
+++ b/resources/qml/ReplyPopup.qml
@@ -21,15 +21,30 @@ Rectangle {
     Reply {
         id: replyPreview
 
+        property var modelData: room ? room.getDump(room.reply, room.id) : {
+        }
+
         visible: room && room.reply
         anchors.left: parent.left
         anchors.leftMargin: 2 * 22 + 3 * 16
         anchors.right: closeReplyButton.left
         anchors.rightMargin: 2 * 22 + 3 * 16
         anchors.bottom: parent.bottom
-        modelData: room ? room.getDump(room.reply, room.id) : {
-        }
         userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.window)
+        blurhash: modelData.blurhash ?? ""
+        body: modelData.body ?? ""
+        formattedBody: modelData.formattedBody ?? ""
+        eventId: modelData.eventId ?? ""
+        filename: modelData.filename ?? ""
+        filesize: modelData.filesize ?? ""
+        proportionalHeight: modelData.proportionalHeight ?? 1
+        type: modelData.type ?? MtxEvent.UnknownMessage
+        typeString: modelData.typeString ?? ""
+        url: modelData.url ?? ""
+        originalWidth: modelData.originalWidth ?? 0
+        isOnlyEmoji: modelData.isOnlyEmoji ?? false
+        userId: modelData.userId ?? ""
+        userName: modelData.userName ?? ""
     }
 
     ImageButton {
diff --git a/resources/qml/StatusIndicator.qml b/resources/qml/StatusIndicator.qml
index 739cc0072dc0b95d05722d965cb2c89aa81158fa..7e471d69232a38ddf9d37a6fe382a4e4b127f9ef 100644
--- a/resources/qml/StatusIndicator.qml
+++ b/resources/qml/StatusIndicator.qml
@@ -9,14 +9,17 @@ import im.nheko 1.0
 ImageButton {
     id: indicator
 
+    required property int status
+    required property string eventId
+
     width: 16
     height: 16
     hoverEnabled: true
-    changeColorOnHover: (model.state == MtxEvent.Read)
-    cursor: (model.state == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor
-    ToolTip.visible: hovered && model.state != MtxEvent.Empty
+    changeColorOnHover: (status == MtxEvent.Read)
+    cursor: (status == MtxEvent.Read) ? Qt.PointingHandCursor : Qt.ArrowCursor
+    ToolTip.visible: hovered && status != MtxEvent.Empty
     ToolTip.text: {
-        switch (model.state) {
+        switch (status) {
         case MtxEvent.Failed:
             return qsTr("Failed");
         case MtxEvent.Sent:
@@ -30,12 +33,12 @@ ImageButton {
         }
     }
     onClicked: {
-        if (model.state == MtxEvent.Read)
-            room.readReceiptsAction(model.id);
+        if (status == MtxEvent.Read)
+            room.readReceiptsAction(eventId);
 
     }
     image: {
-        switch (model.state) {
+        switch (status) {
         case MtxEvent.Failed:
             return ":/icons/icons/ui/remove-symbol.png";
         case MtxEvent.Sent:
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index e66dd2de72efd9f10e39b47ae98f893e5ed32442..c1cdaf9b8765fc6f9e556453c5c9a87ae28fd56d 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -11,6 +11,33 @@ import QtQuick.Window 2.2
 import im.nheko 1.0
 
 Item {
+    id: r
+
+    required property double proportionalHeight
+    required property int type
+    required property string typeString
+    required property int originalWidth
+    required property string blurhash
+    required property string body
+    required property string formattedBody
+    required property string eventId
+    required property string filename
+    required property string filesize
+    required property string url
+    required property string thumbnailUrl
+    required property bool isOnlyEmoji
+    required property bool isSender
+    required property bool isEncrypted
+    required property bool isEditable
+    required property bool isEdited
+    required property string replyTo
+    required property string userId
+    required property string userName
+    required property var reactions
+    required property int trustlevel
+    required property var timestamp
+    required property int status
+
     anchors.left: parent.left
     anchors.right: parent.right
     height: row.height
@@ -28,19 +55,21 @@ Item {
 
     TapHandler {
         acceptedButtons: Qt.RightButton
-        onSingleTapped: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
+        onSingleTapped: messageContextMenu.show(eventId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
         gesturePolicy: TapHandler.ReleaseWithinBounds
     }
 
     TapHandler {
-        onLongPressed: messageContextMenu.show(model.id, model.type, model.isSender, model.isEncrypted, model.isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
-        onDoubleTapped: chat.model.reply = model.id
+        onLongPressed: messageContextMenu.show(eventId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText)
+        onDoubleTapped: chat.model.reply = eventId
         gesturePolicy: TapHandler.ReleaseWithinBounds
     }
 
     RowLayout {
         id: row
 
+        property var replyData: chat.model.getDump(replyTo, eventId)
+
         anchors.rightMargin: 1
         anchors.leftMargin: Nheko.avatarSize + 16
         anchors.left: parent.left
@@ -55,9 +84,23 @@ Item {
 
             // fancy reply, if this is a reply
             Reply {
-                visible: model.replyTo
-                modelData: chat.model.getDump(model.replyTo, model.id)
-                userColor: TimelineManager.userColor(modelData.userId, Nheko.colors.base)
+                visible: replyTo
+                userColor: TimelineManager.userColor(userId, Nheko.colors.base)
+                blurhash: row.replyData.blurhash ?? ""
+                body: row.replyData.body ?? ""
+                formattedBody: row.replyData.formattedBody ?? ""
+                eventId: row.replyData.eventId ?? ""
+                filename: row.replyData.filename ?? ""
+                filesize: row.replyData.filesize ?? ""
+                proportionalHeight: row.replyData.proportionalHeight ?? 1
+                type: row.replyData.type ?? MtxEvent.UnknownMessage
+                typeString: row.replyData.typeString ?? ""
+                url: row.replyData.url ?? ""
+                originalWidth: row.replyData.originalWidth ?? 0
+                isOnlyEmoji: row.replyData.isOnlyEmoji ?? false
+                userId: row.replyData.userId ?? ""
+                userName: row.replyData.userName ?? ""
+                thumbnailUrl: row.replyData.thumbnailUrl ?? ""
             }
 
             // actual message content
@@ -65,14 +108,29 @@ Item {
                 id: contentItem
 
                 width: parent.width
-                modelData: model
+                blurhash: r.blurhash
+                body: r.body
+                formattedBody: r.formattedBody
+                eventId: r.eventId
+                filename: r.filename
+                filesize: r.filesize
+                proportionalHeight: r.proportionalHeight
+                type: r.type
+                typeString: r.typeString ?? ""
+                url: r.url
+                thumbnailUrl: r.thumbnailUrl
+                originalWidth: r.originalWidth
+                isOnlyEmoji: r.isOnlyEmoji
+                userId: r.userId
+                userName: r.userName
+                isReply: false
             }
 
             Reactions {
                 id: reactionRow
 
-                reactions: model.reactions
-                eventId: model.id
+                reactions: r.reactions
+                eventId: r.eventId
             }
 
         }
@@ -81,19 +139,21 @@ Item {
             Layout.alignment: Qt.AlignRight | Qt.AlignTop
             Layout.preferredHeight: 16
             width: 16
+            status: r.status
+            eventId: r.eventId
         }
 
         EncryptionIndicator {
             visible: room.isEncrypted
-            encrypted: model.isEncrypted
-            trust: model.trustlevel
+            encrypted: isEncrypted
+            trust: trustlevel
             Layout.alignment: Qt.AlignRight | Qt.AlignTop
             Layout.preferredHeight: 16
             Layout.preferredWidth: 16
         }
 
         Image {
-            visible: model.isEdited || model.id == chat.model.edit
+            visible: isEdited || eventId == chat.model.edit
             Layout.alignment: Qt.AlignRight | Qt.AlignTop
             Layout.preferredHeight: 16
             Layout.preferredWidth: 16
@@ -101,7 +161,7 @@ Item {
             width: 16
             sourceSize.width: 16
             sourceSize.height: 16
-            source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((model.id == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText)
+            source: "image://colorimage/:/icons/icons/ui/edit.png?" + ((eventId == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText)
             ToolTip.visible: editHovered.hovered
             ToolTip.text: qsTr("Edited")
 
@@ -113,11 +173,11 @@ Item {
 
         Label {
             Layout.alignment: Qt.AlignRight | Qt.AlignTop
-            text: model.timestamp.toLocaleTimeString(Locale.ShortFormat)
+            text: timestamp.toLocaleTimeString(Locale.ShortFormat)
             width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth)
             color: Nheko.inactiveColors.text
             ToolTip.visible: ma.hovered
-            ToolTip.text: Qt.formatDateTime(model.timestamp, Qt.DefaultLocaleLongDate)
+            ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate)
 
             HoverHandler {
                 id: ma
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index 0392c73ac6342fb685d1803d70c73d92d3d3b780..4f2a2836e356b29aa217173e3789db592ff66336 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -7,6 +7,10 @@ import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
 Item {
+    required property string eventId
+    required property string filename
+    required property string filesize
+
     height: row.height + 24
     width: parent ? parent.width : undefined
 
@@ -34,7 +38,7 @@ Item {
             }
 
             TapHandler {
-                onSingleTapped: room.saveMedia(model.data.id)
+                onSingleTapped: room.saveMedia(eventId)
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
@@ -49,20 +53,20 @@ Item {
             id: col
 
             Text {
-                id: filename
+                id: filename_
 
                 Layout.fillWidth: true
-                text: model.data.filename
+                text: filename
                 textFormat: Text.PlainText
                 elide: Text.ElideRight
                 color: Nheko.colors.text
             }
 
             Text {
-                id: filesize
+                id: filesize_
 
                 Layout.fillWidth: true
-                text: model.data.filesize
+                text: filesize
                 textFormat: Text.PlainText
                 elide: Text.ElideRight
                 color: Nheko.colors.text
@@ -77,7 +81,7 @@ Item {
         z: -1
         radius: 10
         height: row.height + 24
-        width: 44 + 24 + 24 + Math.max(Math.min(filesize.width, filesize.implicitWidth), Math.min(filename.width, filename.implicitWidth))
+        width: 44 + 24 + 24 + Math.max(Math.min(filesize_.width, filesize_.implicitWidth), Math.min(filename_.width, filename_.implicitWidth))
     }
 
 }
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index ce8e779cf788746ad5280d86725f19b2e78776c8..b432018c7753c065fc6bb9264a471b68fdeb36de 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -6,20 +6,28 @@ import QtQuick 2.12
 import im.nheko 1.0
 
 Item {
-    property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width)
-    property double tempHeight: tempWidth * model.data.proportionalHeight
-    property double divisor: model.isReply ? 5 : 3
+    required property int type
+    required property int originalWidth
+    required property double proportionalHeight
+    required property string url
+    required property string blurhash
+    required property string body
+    required property string filename
+    required property bool isReply
+    property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 200 : originalWidth)
+    property double tempHeight: tempWidth * proportionalHeight
+    property double divisor: isReply ? 5 : 3
     property bool tooHigh: tempHeight > timelineView.height / divisor
 
     height: Math.round(tooHigh ? timelineView.height / divisor : tempHeight)
-    width: Math.round(tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth)
+    width: Math.round(tooHigh ? (timelineView.height / divisor) / proportionalHeight : tempWidth)
 
     Image {
-        id: blurhash
+        id: blurhash_
 
         anchors.fill: parent
         visible: img.status != Image.Ready
-        source: model.data.blurhash ? ("image://blurhash/" + model.data.blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + Nheko.colors.buttonText)
+        source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/do-not-disturb-rounded-sign@2x.png?" + Nheko.colors.buttonText)
         asynchronous: true
         fillMode: Image.PreserveAspectFit
         sourceSize.width: parent.width
@@ -30,16 +38,16 @@ Item {
         id: img
 
         anchors.fill: parent
-        source: model.data.url.replace("mxc://", "image://MxcImage/")
+        source: url.replace("mxc://", "image://MxcImage/")
         asynchronous: true
         fillMode: Image.PreserveAspectFit
         smooth: true
         mipmap: true
 
         TapHandler {
-            enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready
+            enabled: type == MtxEvent.ImageMessage && img.status == Image.Ready
             onSingleTapped: {
-                TimelineManager.openImageOverlay(model.data.url, model.data.id);
+                TimelineManager.openImageOverlay(url, room.data.eventId);
                 eventPoint.accepted = true;
             }
             gesturePolicy: TapHandler.ReleaseWithinBounds
@@ -73,7 +81,7 @@ Item {
                 horizontalAlignment: Text.AlignHCenter
                 verticalAlignment: Text.AlignVCenter
                 // See this MSC: https://github.com/matrix-org/matrix-doc/pull/2530
-                text: model.data.filename ? model.data.filename : model.data.body
+                text: filename ? filename : body
                 color: Nheko.colors.text
             }
 
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 1befcec374ac01dfa50417d3605cc593df007c98..4b32751cc14cfb6989bf44f684195ca6317e08b5 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -6,32 +6,41 @@ import QtQuick 2.6
 import im.nheko 1.0
 
 Item {
-    property alias modelData: model.data
-    property alias isReply: model.isReply
+    id: d
+
+    required property bool isReply
     property alias child: chooser.child
     property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width
+    required property double proportionalHeight
+    required property int type
+    required property string typeString
+    required property int originalWidth
+    required property string blurhash
+    required property string body
+    required property string formattedBody
+    required property string eventId
+    required property string filename
+    required property string filesize
+    required property string url
+    required property string thumbnailUrl
+    required property bool isOnlyEmoji
+    required property string userId
+    required property string userName
 
     height: chooser.childrenRect.height
 
-    // Workaround to have an assignable global property
-    Item {
-        id: model
-
-        property var data
-        property bool isReply: false
-    }
-
     DelegateChooser {
         id: chooser
 
         //role: "type" //< not supported in our custom implementation, have to use roleValue
-        roleValue: model.data.type
+        roleValue: type
         anchors.fill: parent
 
         DelegateChoice {
             roleValue: MtxEvent.UnknownMessage
 
             Placeholder {
+                typeString: d.typeString
                 text: "Unretrieved event"
             }
 
@@ -41,6 +50,10 @@ Item {
             roleValue: MtxEvent.TextMessage
 
             TextMessage {
+                formatted: d.formattedBody
+                body: d.body
+                isOnlyEmoji: d.isOnlyEmoji
+                isReply: d.isReply
             }
 
         }
@@ -49,6 +62,10 @@ Item {
             roleValue: MtxEvent.NoticeMessage
 
             NoticeMessage {
+                formatted: d.formattedBody
+                body: d.body
+                isOnlyEmoji: d.isOnlyEmoji
+                isReply: d.isReply
             }
 
         }
@@ -57,8 +74,11 @@ Item {
             roleValue: MtxEvent.EmoteMessage
 
             NoticeMessage {
-                formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody
-                color: TimelineManager.userColor(modelData.userId, Nheko.colors.window)
+                formatted: TimelineManager.escapeEmoji(d.userName) + " " + d.formattedBody
+                color: TimelineManager.userColor(d.userId, Nheko.colors.window)
+                body: d.body
+                isOnlyEmoji: d.isOnlyEmoji
+                isReply: d.isReply
             }
 
         }
@@ -67,6 +87,14 @@ Item {
             roleValue: MtxEvent.ImageMessage
 
             ImageMessage {
+                type: d.type
+                originalWidth: d.originalWidth
+                proportionalHeight: d.proportionalHeight
+                url: d.url
+                blurhash: d.blurhash
+                body: d.body
+                filename: d.filename
+                isReply: d.isReply
             }
 
         }
@@ -75,6 +103,14 @@ Item {
             roleValue: MtxEvent.Sticker
 
             ImageMessage {
+                type: d.type
+                originalWidth: d.originalWidth
+                proportionalHeight: d.proportionalHeight
+                url: d.url
+                blurhash: d.blurhash
+                body: d.body
+                filename: d.filename
+                isReply: d.isReply
             }
 
         }
@@ -83,6 +119,9 @@ Item {
             roleValue: MtxEvent.FileMessage
 
             FileMessage {
+                eventId: d.eventId
+                filename: d.filename
+                filesize: d.filesize
             }
 
         }
@@ -91,6 +130,14 @@ Item {
             roleValue: MtxEvent.VideoMessage
 
             PlayableMediaMessage {
+                proportionalHeight: d.proportionalHeight
+                type: d.type
+                originalWidth: d.originalWidth
+                thumbnailUrl: d.thumbnailUrl
+                eventId: d.eventId
+                url: d.url
+                body: d.body
+                filesize: d.filesize
             }
 
         }
@@ -99,6 +146,14 @@ Item {
             roleValue: MtxEvent.AudioMessage
 
             PlayableMediaMessage {
+                proportionalHeight: d.proportionalHeight
+                type: d.type
+                originalWidth: d.originalWidth
+                thumbnailUrl: d.thumbnailUrl
+                eventId: d.eventId
+                url: d.url
+                body: d.body
+                filesize: d.filesize
             }
 
         }
@@ -134,7 +189,10 @@ Item {
             roleValue: MtxEvent.Name
 
             NoticeMessage {
-                text: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name")
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: model.data.roomName ? qsTr("room name changed to: %1").arg(model.data.roomName) : qsTr("removed room name")
             }
 
         }
@@ -143,7 +201,10 @@ Item {
             roleValue: MtxEvent.Topic
 
             NoticeMessage {
-                text: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic")
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: model.data.roomTopic ? qsTr("topic changed to: %1").arg(model.data.roomTopic) : qsTr("removed topic")
             }
 
         }
@@ -152,7 +213,10 @@ Item {
             roleValue: MtxEvent.Avatar
 
             NoticeMessage {
-                text: qsTr("%1 changed the room avatar").arg(model.data.userName)
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: qsTr("%1 changed the room avatar").arg(d.userName)
             }
 
         }
@@ -161,7 +225,10 @@ Item {
             roleValue: MtxEvent.RoomCreate
 
             NoticeMessage {
-                text: qsTr("%1 created and configured room: %2").arg(model.data.userName).arg(model.data.roomId)
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(model.data.roomId)
             }
 
         }
@@ -170,14 +237,17 @@ Item {
             roleValue: MtxEvent.CallInvite
 
             NoticeMessage {
-                text: {
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: {
                     switch (model.data.callType) {
                     case "voice":
-                        return qsTr("%1 placed a voice call.").arg(model.data.userName);
+                        return qsTr("%1 placed a voice call.").arg(d.userName);
                     case "video":
-                        return qsTr("%1 placed a video call.").arg(model.data.userName);
+                        return qsTr("%1 placed a video call.").arg(d.userName);
                     default:
-                        return qsTr("%1 placed a call.").arg(model.data.userName);
+                        return qsTr("%1 placed a call.").arg(d.userName);
                     }
                 }
             }
@@ -188,7 +258,10 @@ Item {
             roleValue: MtxEvent.CallAnswer
 
             NoticeMessage {
-                text: qsTr("%1 answered the call.").arg(model.data.userName)
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: qsTr("%1 answered the call.").arg(d.userName)
             }
 
         }
@@ -197,7 +270,10 @@ Item {
             roleValue: MtxEvent.CallHangUp
 
             NoticeMessage {
-                text: qsTr("%1 ended the call.").arg(model.data.userName)
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: qsTr("%1 ended the call.").arg(d.userName)
             }
 
         }
@@ -206,7 +282,10 @@ Item {
             roleValue: MtxEvent.CallCandidates
 
             NoticeMessage {
-                text: qsTr("Negotiating call...")
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: qsTr("Negotiating call...")
             }
 
         }
@@ -216,7 +295,10 @@ Item {
             roleValue: MtxEvent.PowerLevels
 
             NoticeMessage {
-                text: room.formatPowerLevelEvent(model.data.id)
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: room.formatPowerLevelEvent(d.eventId)
             }
 
         }
@@ -225,7 +307,10 @@ Item {
             roleValue: MtxEvent.RoomJoinRules
 
             NoticeMessage {
-                text: room.formatJoinRuleEvent(model.data.id)
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: room.formatJoinRuleEvent(d.eventId)
             }
 
         }
@@ -234,7 +319,10 @@ Item {
             roleValue: MtxEvent.RoomHistoryVisibility
 
             NoticeMessage {
-                text: room.formatHistoryVisibilityEvent(model.data.id)
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: room.formatHistoryVisibilityEvent(d.eventId)
             }
 
         }
@@ -243,7 +331,10 @@ Item {
             roleValue: MtxEvent.RoomGuestAccess
 
             NoticeMessage {
-                text: room.formatGuestAccessEvent(model.data.id)
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: room.formatGuestAccessEvent(d.eventId)
             }
 
         }
@@ -252,7 +343,10 @@ Item {
             roleValue: MtxEvent.Member
 
             NoticeMessage {
-                text: room.formatMemberEvent(model.data.id)
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: room.formatMemberEvent(d.eventId)
             }
 
         }
@@ -261,7 +355,10 @@ Item {
             roleValue: MtxEvent.KeyVerificationRequest
 
             NoticeMessage {
-                text: "KeyVerificationRequest"
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: "KeyVerificationRequest"
             }
 
         }
@@ -270,7 +367,10 @@ Item {
             roleValue: MtxEvent.KeyVerificationStart
 
             NoticeMessage {
-                text: "KeyVerificationStart"
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: "KeyVerificationStart"
             }
 
         }
@@ -279,7 +379,10 @@ Item {
             roleValue: MtxEvent.KeyVerificationReady
 
             NoticeMessage {
-                text: "KeyVerificationReady"
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: "KeyVerificationReady"
             }
 
         }
@@ -288,7 +391,10 @@ Item {
             roleValue: MtxEvent.KeyVerificationCancel
 
             NoticeMessage {
-                text: "KeyVerificationCancel"
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: "KeyVerificationCancel"
             }
 
         }
@@ -297,7 +403,10 @@ Item {
             roleValue: MtxEvent.KeyVerificationKey
 
             NoticeMessage {
-                text: "KeyVerificationKey"
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: "KeyVerificationKey"
             }
 
         }
@@ -306,7 +415,10 @@ Item {
             roleValue: MtxEvent.KeyVerificationMac
 
             NoticeMessage {
-                text: "KeyVerificationMac"
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: "KeyVerificationMac"
             }
 
         }
@@ -315,7 +427,10 @@ Item {
             roleValue: MtxEvent.KeyVerificationDone
 
             NoticeMessage {
-                text: "KeyVerificationDone"
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: "KeyVerificationDone"
             }
 
         }
@@ -324,7 +439,10 @@ Item {
             roleValue: MtxEvent.KeyVerificationDone
 
             NoticeMessage {
-                text: "KeyVerificationDone"
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: "KeyVerificationDone"
             }
 
         }
@@ -333,13 +451,17 @@ Item {
             roleValue: MtxEvent.KeyVerificationAccept
 
             NoticeMessage {
-                text: "KeyVerificationAccept"
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: "KeyVerificationAccept"
             }
 
         }
 
         DelegateChoice {
             Placeholder {
+                typeString: d.typeString
             }
 
         }
diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml
index c4fc6cc35eb50b7b19c153b053f12a6b16bf1839..692fe4a942680aad31d71b3ddaeb7a78ce25eeb7 100644
--- a/resources/qml/delegates/Placeholder.qml
+++ b/resources/qml/delegates/Placeholder.qml
@@ -6,7 +6,9 @@ import ".."
 import im.nheko 1.0
 
 MatrixText {
-    text: qsTr("unimplemented event: ") + model.data.typeString
+    required property string typeString
+
+    text: qsTr("unimplemented event: ") + typeString
     width: parent ? parent.width : undefined
     color: Nheko.inactiveColors.text
 }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 83864db91865531f28732540915dd93866d1df62..fd764d522a994e5e5b7d8360145b550efb0608d9 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -12,10 +12,21 @@ import im.nheko 1.0
 Rectangle {
     id: bg
 
+    required property double proportionalHeight
+    required property int type
+    required property int originalWidth
+    required property string thumbnailUrl
+    required property string eventId
+    required property string url
+    required property string body
+    required property string filesize
+
     radius: 10
     color: Nheko.colors.alternateBase
     height: Math.round(content.height + 24)
     width: parent ? parent.width : undefined
+    ListView.onPooled: height = 4
+    ListView.onReused: height = Math.round(content.height + 24)
 
     Column {
         id: content
@@ -26,18 +37,18 @@ Rectangle {
         Rectangle {
             id: videoContainer
 
-            property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width)
-            property double tempHeight: tempWidth * model.data.proportionalHeight
-            property double divisor: model.isReply ? 4 : 2
+            property double tempWidth: Math.min(parent ? parent.width : undefined, originalWidth < 1 ? 400 : originalWidth)
+            property double tempHeight: tempWidth * proportionalHeight
+            property double divisor: isReply ? 4 : 2
             property bool tooHigh: tempHeight > timelineView.height / divisor
 
-            visible: model.data.type == MtxEvent.VideoMessage
+            visible: type == MtxEvent.VideoMessage
             height: tooHigh ? timelineView.height / divisor : tempHeight
-            width: tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth
+            width: tooHigh ? (timelineView.height / divisor) / proportionalHeight : tempWidth
 
             Image {
                 anchors.fill: parent
-                source: model.data.thumbnailUrl.replace("mxc://", "image://MxcImage/")
+                source: thumbnailUrl.replace("mxc://", "image://MxcImage/")
                 asynchronous: true
                 fillMode: Image.PreserveAspectFit
 
@@ -121,7 +132,7 @@ Rectangle {
                 onClicked: {
                     switch (button.state) {
                     case "":
-                        room.cacheMedia(model.data.id);
+                        room.cacheMedia(eventId);
                         break;
                     case "stopped":
                         media.play();
@@ -176,7 +187,7 @@ Rectangle {
                 Connections {
                     target: room
                     onMediaCached: {
-                        if (mxcUrl == model.data.url) {
+                        if (mxcUrl == url) {
                             media.source = cacheUrl;
                             button.state = "stopped";
                             console.log("media loaded: " + mxcUrl + " at " + cacheUrl);
@@ -192,14 +203,14 @@ Rectangle {
 
                 Text {
                     Layout.fillWidth: true
-                    text: model.data.body
+                    text: body
                     elide: Text.ElideRight
                     color: Nheko.colors.text
                 }
 
                 Text {
                     Layout.fillWidth: true
-                    text: model.data.filesize
+                    text: filesize
                     textFormat: Text.PlainText
                     elide: Text.ElideRight
                     color: Nheko.colors.text
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index b509052953b4193c5b865ac5a9521e2205fde500..08f13955058d1a89a959c4048a81aece18ad095e 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -9,16 +9,30 @@ import QtQuick.Window 2.2
 import im.nheko 1.0
 
 Item {
-    id: replyComponent
+    id: r
 
-    property alias modelData: reply.modelData
     property color userColor: "red"
+    property double proportionalHeight
+    property int type
+    property string typeString
+    property int originalWidth
+    property string blurhash
+    property string body
+    property string formattedBody
+    property string eventId
+    property string filename
+    property string filesize
+    property string url
+    property bool isOnlyEmoji
+    property string userId
+    property string userName
+    property string thumbnailUrl
 
     width: parent.width
     height: replyContainer.height
 
     TapHandler {
-        onSingleTapped: chat.model.showEvent(modelData.id)
+        onSingleTapped: chat.model.showEvent(eventId)
         gesturePolicy: TapHandler.ReleaseWithinBounds
     }
 
@@ -33,7 +47,7 @@ Item {
         anchors.top: replyContainer.top
         anchors.bottom: replyContainer.bottom
         width: 4
-        color: TimelineManager.userColor(reply.modelData.userId, Nheko.colors.window)
+        color: TimelineManager.userColor(userId, Nheko.colors.window)
     }
 
     Column {
@@ -44,14 +58,14 @@ Item {
         width: parent.width - 8
 
         Text {
-            id: userName
+            id: userName_
 
-            text: TimelineManager.escapeEmoji(reply.modelData.userName)
-            color: replyComponent.userColor
+            text: TimelineManager.escapeEmoji(userName)
+            color: r.userColor
             textFormat: Text.RichText
 
             TapHandler {
-                onSingleTapped: chat.model.openUserProfile(reply.modelData.userId)
+                onSingleTapped: chat.model.openUserProfile(userId)
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
@@ -60,6 +74,21 @@ Item {
         MessageDelegate {
             id: reply
 
+            blurhash: r.blurhash
+            body: r.body
+            formattedBody: r.formattedBody
+            eventId: r.eventId
+            filename: r.filename
+            filesize: r.filesize
+            proportionalHeight: r.proportionalHeight
+            type: r.type
+            typeString: r.typeString ?? ""
+            url: r.url
+            thumbnailUrl: r.thumbnailUrl
+            originalWidth: r.originalWidth
+            isOnlyEmoji: r.isOnlyEmoji
+            userId: r.userId
+            userName: r.userName
             enabled: false
             width: parent.width
             isReply: true
@@ -72,7 +101,7 @@ Item {
 
         z: -1
         height: replyContainer.height
-        width: Math.min(Math.max(reply.implicitWidth, userName.implicitWidth) + 8 + 4, parent.width)
+        width: Math.min(Math.max(reply.implicitWidth, userName_.implicitWidth) + 8 + 4, parent.width)
         color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.1)
     }
 
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index cd46f8caa0e8957280444b54d08fdb3bf1e7a499..58aa99cac551a7a9faf49aa64cff82145a960558 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -6,8 +6,11 @@ import ".."
 import im.nheko 1.0
 
 MatrixText {
-    property string formatted: model.data.formattedBody
-    property string copyText: selectedText ? getText(selectionStart, selectionEnd) : model.data.body
+    required property string body
+    required property bool isOnlyEmoji
+    required property bool isReply
+    required property string formatted
+    property string copyText: selectedText ? getText(selectionStart, selectionEnd) : body
 
     // table border-collapse doesn't seem to work
     text: "
@@ -31,5 +34,5 @@ MatrixText {
     height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
     clip: isReply
     selectByMouse: !Settings.mobileMode && !isReply
-    font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
+    font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
 }
diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml
index 3106c38268c1506beb2012da42a9a3cd37a87c03..d44c5edf9e638cf1c83b8c7e48d0faa376663260 100644
--- a/resources/qml/voip/ActiveCallBar.qml
+++ b/resources/qml/voip/ActiveCallBar.qml
@@ -35,7 +35,7 @@ Rectangle {
             height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
             displayName: CallManager.callParty
-            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
+            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
         }
 
         Label {
diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml
index 2d8e3040cbee99074ebbea8bc200126434adbbd4..f6c1ecde8dc5034b2297f57fd37e6a576e58c0ff 100644
--- a/resources/qml/voip/CallInviteBar.qml
+++ b/resources/qml/voip/CallInviteBar.qml
@@ -42,7 +42,7 @@ Rectangle {
             height: Nheko.avatarSize
             url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
             displayName: CallManager.callParty
-            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
+            onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
         }
 
         Label {
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index 97e39e021be186b3e1ceee918735bbfc946ae5ad..5f56485346b0b4a3723af29d90073511b9b0aff0 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -79,7 +79,7 @@ Popup {
                 height: Nheko.avatarSize
                 url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/")
                 displayName: room.roomName
-                onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.id)
+                onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId)
             }
 
             Button {
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 4cb97e0750773995e4bd24082c9361bfe1129606..ab11f99bc1943bbb3f9cd26f52a538127a40c773 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -427,11 +427,11 @@ TimelineModel::roleNames() const
           {Filename, "filename"},
           {Filesize, "filesize"},
           {MimeType, "mimetype"},
-          {Height, "height"},
-          {Width, "width"},
+          {OriginalHeight, "originalHeight"},
+          {OriginalWidth, "originalWidth"},
           {ProportionalHeight, "proportionalHeight"},
-          {Id, "id"},
-          {State, "state"},
+          {EventId, "eventId"},
+          {State, "status"},
           {IsEdited, "isEdited"},
           {IsEditable, "isEditable"},
           {IsEncrypted, "isEncrypted"},
@@ -556,9 +556,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                 return QVariant(utils::humanReadableFileSize(filesize(event)));
         case MimeType:
                 return QVariant(QString::fromStdString(mimetype(event)));
-        case Height:
+        case OriginalHeight:
                 return QVariant(qulonglong{media_height(event)});
-        case Width:
+        case OriginalWidth:
                 return QVariant(qulonglong{media_width(event)});
         case ProportionalHeight: {
                 auto w = media_width(event);
@@ -569,7 +569,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
 
                 return QVariant(prop > 0 ? prop : 1.);
         }
-        case Id: {
+        case EventId: {
                 if (auto replaces = relations(event).replaces())
                         return QVariant(QString::fromStdString(replaces.value()));
                 else
@@ -660,11 +660,11 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                 m.insert(names[Filename], data(event, static_cast<int>(Filename)));
                 m.insert(names[Filesize], data(event, static_cast<int>(Filesize)));
                 m.insert(names[MimeType], data(event, static_cast<int>(MimeType)));
-                m.insert(names[Height], data(event, static_cast<int>(Height)));
-                m.insert(names[Width], data(event, static_cast<int>(Width)));
+                m.insert(names[OriginalHeight], data(event, static_cast<int>(OriginalHeight)));
+                m.insert(names[OriginalWidth], data(event, static_cast<int>(OriginalWidth)));
                 m.insert(names[ProportionalHeight],
                          data(event, static_cast<int>(ProportionalHeight)));
-                m.insert(names[Id], data(event, static_cast<int>(Id)));
+                m.insert(names[EventId], data(event, static_cast<int>(EventId)));
                 m.insert(names[State], data(event, static_cast<int>(State)));
                 m.insert(names[IsEdited], data(event, static_cast<int>(IsEdited)));
                 m.insert(names[IsEditable], data(event, static_cast<int>(IsEditable)));
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index f093acb4603a75616a7d9e99b2eb715c02c2066e..46153732c1fdc4285e50cad14590a618dd1efdb3 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -192,10 +192,10 @@ public:
                 Filename,
                 Filesize,
                 MimeType,
-                Height,
-                Width,
+                OriginalHeight,
+                OriginalWidth,
                 ProportionalHeight,
-                Id,
+                EventId,
                 State,
                 IsEdited,
                 IsEditable,
@@ -245,6 +245,11 @@ public:
         Q_INVOKABLE void showEvent(QString eventId);
         Q_INVOKABLE void copyLinkToEvent(QString eventId) const;
         void cacheMedia(QString eventId, std::function<void(const QString filename)> callback);
+        Q_INVOKABLE void sendReset()
+        {
+                beginResetModel();
+                endResetModel();
+        }
 
         std::vector<::Reaction> reactions(const std::string &event_id)
         {