summary refs log tree commit diff
path: root/resources/qml/TimelineView.qml
diff options
context:
space:
mode:
authorNicolas Werner <nicolas.werner@hotmail.de>2020-10-08 21:11:21 +0200
committerNicolas Werner <nicolas.werner@hotmail.de>2020-10-08 21:11:21 +0200
commit1a029112d9e6168b123f6cdf4b49319ce4ea356d (patch)
treea4797860c06eeff4db1154132a084dccc9a963d9 /resources/qml/TimelineView.qml
parentMerge pull request #270 from Chethan2k1/device-verification (diff)
downloadnheko-1a029112d9e6168b123f6cdf4b49319ce4ea356d.tar.xz
Lint qml with qml-format
Diffstat (limited to 'resources/qml/TimelineView.qml')
-rw-r--r--resources/qml/TimelineView.qml1074
1 files changed, 565 insertions, 509 deletions
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 5c9ca348..ab0148e9 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -1,526 +1,582 @@
+import "./delegates"
+import "./device-verification"
+import "./emoji"
+import QtGraphicalEffects 1.0
 import QtQuick 2.9
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
-import QtGraphicalEffects 1.0
 import QtQuick.Window 2.2
-
 import im.nheko 1.0
 import im.nheko.EmojiModel 1.0
 
-import "./delegates"
-import "./emoji"
-import "./device-verification"
-
 Page {
-	id: timelineRoot
-
-	property var colors: currentActivePalette
-	property var systemInactive: SystemPalette { colorGroup: SystemPalette.Disabled }
-	property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
-	property int avatarSize: 40
-	property real highlightHue: colors.highlight.hslHue
-	property real highlightSat: colors.highlight.hslSaturation
-	property real highlightLight: colors.highlight.hslLightness
-
-	palette: colors
-
-	FontMetrics {
-		id: fontMetrics
-	}
-
-	EmojiPicker {
-		id: emojiPopup
-		width: 7 * 52 + 20
-		height: 6 * 52 
-		colors: palette
-		model: EmojiProxyModel {
-			category: EmojiCategory.People
-			sourceModel: EmojiModel {}
-		}
-	}
-
-	Menu {
-		id: messageContextMenu
-		modal: true
-
-		function show(eventId_, eventType_, isEncrypted_, showAt_, position) {
-			eventId = eventId_
-			eventType = eventType_
-			isEncrypted = isEncrypted_
-
-			if (position)
-			popup(position, showAt_)
-			else
-			popup(showAt_)
-		}
-
-		property string eventId
-		property int eventType
-		property bool isEncrypted
-
-		MenuItem {
-			text: qsTr("React")
-			onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId)
-		}
-		MenuItem {
-			text: qsTr("Reply")
-			onClicked: chat.model.replyAction(messageContextMenu.eventId)
-		}
-		MenuItem {
-			text: qsTr("Read receipts")
-			onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId)
-		}
-		MenuItem {
-			text: qsTr("Mark as read")
-		}
-		MenuItem {
-			text: qsTr("View raw message")
-			onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId)
-		}
-		MenuItem {
-			visible: messageContextMenu.isEncrypted
-			height: visible ? implicitHeight : 0
-			text: qsTr("View decrypted raw message")
-			onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId)
-		}
-		MenuItem {
-			text: qsTr("Redact message")
-			onTriggered: chat.model.redactEvent(messageContextMenu.eventId)
-		}
-		MenuItem {
-			visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
-			height: visible ? implicitHeight : 0
-			text: qsTr("Save as")
-			onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
-		}
-	}
-
-	Rectangle {
-		anchors.fill: parent
-		color: colors.window
-
-		Component {
-			id: deviceVerificationDialog
-			DeviceVerification {}
-		}
-		Connections {
-			target: TimelineManager
-			function onNewDeviceVerificationRequest(flow,transactionId,userId,deviceId,isRequest) {
-				var dialog = deviceVerificationDialog.createObject(timelineRoot, {flow: flow});
-				dialog.show();
-			}
-		}
-		Connections {
-			target: TimelineManager.timeline
-			function onOpenProfile(profile) {
-				var userProfile = userProfileComponent.createObject(timelineRoot,{profile: profile});
-				userProfile.show();
-			}
-		}
-
-		Label {
-			visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
-			anchors.centerIn: parent
-			text: qsTr("No room open")
-			font.pointSize: 24
-			color: colors.text
-		}
-
-		BusyIndicator {
-			visible: running
-			anchors.centerIn: parent
-			running: TimelineManager.isInitialSync
-			height: 200
-			width: 200
-			z: 3
-		}
-
-		ColumnLayout {
-			anchors.fill: parent
-			Rectangle {
-				id: topBar
-
-				Layout.fillWidth: true
-				implicitHeight: topLayout.height + 16
-				z: 3
-
-				color: colors.base
-
-				MouseArea {
-					anchors.fill: parent
-					onClicked: TimelineManager.openRoomSettings();
-				}
-
-				GridLayout {
-					id: topLayout
-
-					anchors.left: parent.left
-					anchors.right: parent.right
-					anchors.margins: 8
-					anchors.verticalCenter: parent.verticalCenter
-
-					//Layout.margins: 8
-
-					ImageButton {
-						id: backToRoomsButton
-
-						Layout.column: 0
-						Layout.row: 0
-						Layout.rowSpan: 2
-						Layout.alignment: Qt.AlignVCenter
-
-						visible: TimelineManager.isNarrowView
-
-						image: ":/icons/icons/ui/angle-pointing-to-left.png"
-
-						ToolTip.visible: hovered
-						ToolTip.text: qsTr("Back to room list")
-
-						onClicked: TimelineManager.backToRooms()
-					}
-
-					Avatar {
-						Layout.column: 1
-						Layout.row: 0
-						Layout.rowSpan: 2
-						Layout.alignment: Qt.AlignVCenter
-
-						width: avatarSize
-						height: avatarSize
-
-						url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
-						displayName: chat.model ? chat.model.roomName : qsTr("No room selected")
-
-						MouseArea {
-							anchors.fill: parent
-							onClicked: TimelineManager.openRoomSettings();
-						}
-					}
-
-					Label {
-						Layout.fillWidth: true
-						Layout.column: 2
-						Layout.row: 0
-						color: colors.text
-
-						font.pointSize: fontMetrics.font.pointSize * 1.1
-
-						text: chat.model ? chat.model.roomName : qsTr("No room selected")
-
-						MouseArea {
-							anchors.fill: parent
-							onClicked: TimelineManager.openRoomSettings();
-						}
-					}
-					MatrixText {
-						Layout.fillWidth: true
-						Layout.column: 2
-						Layout.row: 1
-						Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
-						clip: true
-
-						text: chat.model ? chat.model.roomTopic : ""
-					}
-
-					ImageButton {
-						id: roomOptionsButton
-
-						Layout.column: 3
-						Layout.row: 0
-						Layout.rowSpan: 2
-						Layout.alignment: Qt.AlignVCenter
-
-						image: ":/icons/icons/ui/vertical-ellipsis.png"
-
-						ToolTip.visible: hovered
-						ToolTip.text: qsTr("Room options")
-
-						onClicked: roomOptionsMenu.popup(roomOptionsButton)
-
-						Menu {
-							id: roomOptionsMenu
-							MenuItem {
-								text: qsTr("Invite users")
-								onTriggered: TimelineManager.openInviteUsersDialog();
-							}
-							MenuItem {
-								text: qsTr("Members")
-								onTriggered: TimelineManager.openMemberListDialog();
-							}
-							MenuItem {
-								text: qsTr("Leave room")
-								onTriggered: TimelineManager.openLeaveRoomDialog();
-							}
-							MenuItem {
-								text: qsTr("Settings")
-								onTriggered: TimelineManager.openRoomSettings();
-							}
-						}
-					}
-				}
-			}
-
-		ListView {
-			id: chat
-
-			visible: TimelineManager.timeline != null
-
-			cacheBuffer: 400
-
-			Layout.fillWidth: true
-			Layout.fillHeight: true
-
-			model: TimelineManager.timeline
-
-			boundsBehavior: Flickable.StopAtBounds
-
-			ScrollHelper {
-				flickable: parent
-				anchors.fill: parent
-			}
-
-			pixelAligned: true
-
-			Shortcut {
-				sequence: StandardKey.MoveToPreviousPage
-				onActivated: { chat.contentY = chat.contentY - chat.height / 2; chat.returnToBounds(); }
-			}
-			Shortcut {
-				sequence: StandardKey.MoveToNextPage
-				onActivated: { chat.contentY = chat.contentY + chat.height / 2; chat.returnToBounds(); }
-			}
-			Shortcut {
-				sequence: StandardKey.Cancel
-				onActivated: chat.model.reply = undefined
-			}
-			Shortcut {
-				sequence: "Alt+Up"
-				onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply? chat.model.idToIndex(chat.model.reply) + 1 : 0)
-			}
-			Shortcut {
-				sequence: "Alt+Down"
-				onActivated: {
-					var idx = chat.model.reply? chat.model.idToIndex(chat.model.reply) - 1 : -1
-					chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined
-				}
-			}
-
-			ScrollBar.vertical: ScrollBar {
-				id: scrollbar
-			}
-
-			spacing: 4
-			verticalLayoutDirection: ListView.BottomToTop
-
-			onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom
-
-			property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width*2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width*2)
-
-			delegate: Item {
-				// This would normally be previousSection, but our model's order is inverted.
-				property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
-
-				id: wrapper
-				property Item section
-				anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-				width: chat.delegateMaxWidth
-				height: section ? section.height + timelinerow.height : timelinerow.height
-
-				TimelineRow {
-					id: timelinerow
-					y: section ? section.y + section.height : 0
-				}
-
-				onSectionBoundaryChanged: {
-					if (sectionBoundary) {
-						var properties = {
-							'modelData': model.dump,
-							'section': ListView.section,
-							'nextSection': ListView.nextSection
-						}
-						section = sectionHeader.createObject(wrapper, properties)
-					} else {
-						section.destroy()
-						section = null
-					}
-				}
-
-				Connections {
-					target: chat
-					function onMovementEnded() {
-						if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
-							chat.model.currentIndex = index;
-					}
-				}
-			}
-
-			Component{
-				id: userProfileComponent
-				UserProfile{}
-			}
-
-			section {
-				property: "section"
-			}
-			Component {
-				id: sectionHeader
-				Column {
-					property var modelData
-					property string section
-					property string nextSection
-
-					topPadding: 4
-					bottomPadding: 4
-					spacing: 8
-
-					visible: !!modelData
-
-					width: parent.width
-					height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
-
-					Label {
-						id: dateBubble
-						anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
-						visible: section.includes(" ")
-						text: chat.model.formatDateSeparator(modelData.timestamp)
-						color: colors.text
-
-                        height: fontMetrics.height * 1.4
-                        width: contentWidth * 1.2
-
-                        horizontalAlignment: Text.AlignHCenter
-                        verticalAlignment: Text.AlignVCenter
-						background: Rectangle {
-							radius: parent.height / 2
-							color: colors.base
-						}
-					}
-
-					Row {
-						height: userName.height
-						spacing: 8
-
-						Avatar {
-							width: avatarSize
-							height: avatarSize
-							url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
-							displayName: modelData.userName
-							userid: modelData.userId
-
-							MouseArea {
-								anchors.fill: parent
-								onClicked: chat.model.openUserProfile(modelData.userId)
-								cursorShape: Qt.PointingHandCursor
-								propagateComposedEvents: true
-							}
-						}
-
-						Label { 
-							id: userName
-							text: TimelineManager.escapeEmoji(modelData.userName)
-							color: TimelineManager.userColor(modelData.userId, colors.window)
-							textFormat: Text.RichText
-
-							MouseArea {
-								anchors.fill: parent
-								Layout.alignment: Qt.AlignHCenter
-								onClicked: chat.model.openUserProfile(modelData.userId)
-								cursorShape: Qt.PointingHandCursor
-								propagateComposedEvents: true
-							}
-						}
-					}
-				}
-			}
-
-			footer:  BusyIndicator {
-				anchors.horizontalCenter: parent.horizontalCenter
-				running: chat.model && chat.model.paginationInProgress
-				height: 50
-				width: 50
-				z: 3
-			}
-		}
-
-		Item {
-			id: chatFooter
-
-			implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height)
-			Layout.fillWidth: true
-			z: 3
-
-			Column {
-				id: footerContent
-				anchors.left: parent.left
-				anchors.right: parent.right
-				anchors.bottom: parent.bottom
-                Rectangle {
-                    id: typingRect
+    id: timelineRoot
+
+    property var colors: currentActivePalette
+    property var systemInactive
+    property var inactiveColors: currentInactivePalette ? currentInactivePalette : systemInactive
+    property int avatarSize: 40
+    property real highlightHue: colors.highlight.hslHue
+    property real highlightSat: colors.highlight.hslSaturation
+    property real highlightLight: colors.highlight.hslLightness
+
+    palette: colors
+
+    FontMetrics {
+        id: fontMetrics
+    }
+
+    EmojiPicker {
+        id: emojiPopup
+
+        width: 7 * 52 + 20
+        height: 6 * 52
+        colors: palette
+
+        model: EmojiProxyModel {
+            category: EmojiCategory.People
+
+            sourceModel: EmojiModel {
+            }
+
+        }
+
+    }
+
+    Menu {
+        id: messageContextMenu
+
+        property string eventId
+        property int eventType
+        property bool isEncrypted
+
+        function show(eventId_, eventType_, isEncrypted_, showAt_, position) {
+            eventId = eventId_;
+            eventType = eventType_;
+            isEncrypted = isEncrypted_;
+            if (position)
+                popup(position, showAt_);
+            else
+                popup(showAt_);
+        }
+
+        modal: true
+
+        MenuItem {
+            text: qsTr("React")
+            onClicked: emojiPopup.show(messageContextMenu.parent, messageContextMenu.eventId)
+        }
+
+        MenuItem {
+            text: qsTr("Reply")
+            onClicked: chat.model.replyAction(messageContextMenu.eventId)
+        }
+
+        MenuItem {
+            text: qsTr("Read receipts")
+            onTriggered: chat.model.readReceiptsAction(messageContextMenu.eventId)
+        }
+
+        MenuItem {
+            text: qsTr("Mark as read")
+        }
+
+        MenuItem {
+            text: qsTr("View raw message")
+            onTriggered: chat.model.viewRawMessage(messageContextMenu.eventId)
+        }
+
+        MenuItem {
+            visible: messageContextMenu.isEncrypted
+            height: visible ? implicitHeight : 0
+            text: qsTr("View decrypted raw message")
+            onTriggered: chat.model.viewDecryptedRawMessage(messageContextMenu.eventId)
+        }
+
+        MenuItem {
+            text: qsTr("Redact message")
+            onTriggered: chat.model.redactEvent(messageContextMenu.eventId)
+        }
+
+        MenuItem {
+            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
+            height: visible ? implicitHeight : 0
+            text: qsTr("Save as")
+            onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId)
+        }
+
+    }
+
+    Rectangle {
+        anchors.fill: parent
+        color: colors.window
+
+        Component {
+            id: deviceVerificationDialog
+
+            DeviceVerification {
+            }
+
+        }
+
+        Connections {
+            function onNewDeviceVerificationRequest(flow, transactionId, userId, deviceId, isRequest) {
+                var dialog = deviceVerificationDialog.createObject(timelineRoot, {
+                    "flow": flow
+                });
+                dialog.show();
+            }
+
+            target: TimelineManager
+        }
+
+        Connections {
+            function onOpenProfile(profile) {
+                var userProfile = userProfileComponent.createObject(timelineRoot, {
+                    "profile": profile
+                });
+                userProfile.show();
+            }
+
+            target: TimelineManager.timeline
+        }
+
+        Label {
+            visible: !TimelineManager.timeline && !TimelineManager.isInitialSync
+            anchors.centerIn: parent
+            text: qsTr("No room open")
+            font.pointSize: 24
+            color: colors.text
+        }
+
+        BusyIndicator {
+            visible: running
+            anchors.centerIn: parent
+            running: TimelineManager.isInitialSync
+            height: 200
+            width: 200
+            z: 3
+        }
+
+        ColumnLayout {
+            anchors.fill: parent
+
+            Rectangle {
+                id: topBar
+
+                Layout.fillWidth: true
+                implicitHeight: topLayout.height + 16
+                z: 3
+                color: colors.base
+
+                MouseArea {
+                    anchors.fill: parent
+                    onClicked: TimelineManager.openRoomSettings()
+                }
+
+                GridLayout {
+                    //Layout.margins: 8
+
+                    id: topLayout
+
                     anchors.left: parent.left
                     anchors.right: parent.right
-                    color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent"
-                    height: typingDisplay.height
+                    anchors.margins: 8
+                    anchors.verticalCenter: parent.verticalCenter
+
+                    ImageButton {
+                        id: backToRoomsButton
+
+                        Layout.column: 0
+                        Layout.row: 0
+                        Layout.rowSpan: 2
+                        Layout.alignment: Qt.AlignVCenter
+                        visible: TimelineManager.isNarrowView
+                        image: ":/icons/icons/ui/angle-pointing-to-left.png"
+                        ToolTip.visible: hovered
+                        ToolTip.text: qsTr("Back to room list")
+                        onClicked: TimelineManager.backToRooms()
+                    }
+
+                    Avatar {
+                        Layout.column: 1
+                        Layout.row: 0
+                        Layout.rowSpan: 2
+                        Layout.alignment: Qt.AlignVCenter
+                        width: avatarSize
+                        height: avatarSize
+                        url: chat.model ? chat.model.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
+                        displayName: chat.model ? chat.model.roomName : qsTr("No room selected")
+
+                        MouseArea {
+                            anchors.fill: parent
+                            onClicked: TimelineManager.openRoomSettings()
+                        }
+
+                    }
+
                     Label {
-                        id: typingDisplay
+                        Layout.fillWidth: true
+                        Layout.column: 2
+                        Layout.row: 0
+                        color: colors.text
+                        font.pointSize: fontMetrics.font.pointSize * 1.1
+                        text: chat.model ? chat.model.roomName : qsTr("No room selected")
+
+                        MouseArea {
+                            anchors.fill: parent
+                            onClicked: TimelineManager.openRoomSettings()
+                        }
+
+                    }
+
+                    MatrixText {
+                        Layout.fillWidth: true
+                        Layout.column: 2
+                        Layout.row: 1
+                        Layout.maximumHeight: fontMetrics.lineSpacing * 2 // show 2 lines
+                        clip: true
+                        text: chat.model ? chat.model.roomTopic : ""
+                    }
+
+                    ImageButton {
+                        id: roomOptionsButton
+
+                        Layout.column: 3
+                        Layout.row: 0
+                        Layout.rowSpan: 2
+                        Layout.alignment: Qt.AlignVCenter
+                        image: ":/icons/icons/ui/vertical-ellipsis.png"
+                        ToolTip.visible: hovered
+                        ToolTip.text: qsTr("Room options")
+                        onClicked: roomOptionsMenu.popup(roomOptionsButton)
+
+                        Menu {
+                            id: roomOptionsMenu
+
+                            MenuItem {
+                                text: qsTr("Invite users")
+                                onTriggered: TimelineManager.openInviteUsersDialog()
+                            }
+
+                            MenuItem {
+                                text: qsTr("Members")
+                                onTriggered: TimelineManager.openMemberListDialog()
+                            }
+
+                            MenuItem {
+                                text: qsTr("Leave room")
+                                onTriggered: TimelineManager.openLeaveRoomDialog()
+                            }
+
+                            MenuItem {
+                                text: qsTr("Settings")
+                                onTriggered: TimelineManager.openRoomSettings()
+                            }
+
+                        }
+
+                    }
+
+                }
+
+            }
+
+            ListView {
+                id: chat
+
+                property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2)
+
+                visible: TimelineManager.timeline != null
+                cacheBuffer: 400
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                model: TimelineManager.timeline
+                boundsBehavior: Flickable.StopAtBounds
+                pixelAligned: true
+                spacing: 4
+                verticalLayoutDirection: ListView.BottomToTop
+                onCountChanged: {
+                    if (atYEnd)
+                        model.currentIndex = 0;
+
+                } // Mark last event as read, since we are at the bottom
+
+                ScrollHelper {
+                    flickable: parent
+                    anchors.fill: parent
+                }
+
+                Shortcut {
+                    sequence: StandardKey.MoveToPreviousPage
+                    onActivated: {
+                        chat.contentY = chat.contentY - chat.height / 2;
+                        chat.returnToBounds();
+                    }
+                }
+
+                Shortcut {
+                    sequence: StandardKey.MoveToNextPage
+                    onActivated: {
+                        chat.contentY = chat.contentY + chat.height / 2;
+                        chat.returnToBounds();
+                    }
+                }
+
+                Shortcut {
+                    sequence: StandardKey.Cancel
+                    onActivated: chat.model.reply = undefined
+                }
+
+                Shortcut {
+                    sequence: "Alt+Up"
+                    onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
+                }
+
+                Shortcut {
+                    sequence: "Alt+Down"
+                    onActivated: {
+                        var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
+                        chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
+                    }
+                }
+
+                Component {
+                    id: userProfileComponent
+
+                    UserProfile {
+                    }
+
+                }
+
+                section {
+                    property: "section"
+                }
+
+                Component {
+                    id: sectionHeader
+
+                    Column {
+                        property var modelData
+                        property string section
+                        property string nextSection
+
+                        topPadding: 4
+                        bottomPadding: 4
+                        spacing: 8
+                        visible: !!modelData
+                        width: parent.width
+                        height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
+
+                        Label {
+                            id: dateBubble
+
+                            anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
+                            visible: section.includes(" ")
+                            text: chat.model.formatDateSeparator(modelData.timestamp)
+                            color: colors.text
+                            height: fontMetrics.height * 1.4
+                            width: contentWidth * 1.2
+                            horizontalAlignment: Text.AlignHCenter
+                            verticalAlignment: Text.AlignVCenter
+
+                            background: Rectangle {
+                                radius: parent.height / 2
+                                color: colors.base
+                            }
+
+                        }
+
+                        Row {
+                            height: userName.height
+                            spacing: 8
+
+                            Avatar {
+                                width: avatarSize
+                                height: avatarSize
+                                url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
+                                displayName: modelData.userName
+                                userid: modelData.userId
+
+                                MouseArea {
+                                    anchors.fill: parent
+                                    onClicked: chat.model.openUserProfile(modelData.userId)
+                                    cursorShape: Qt.PointingHandCursor
+                                    propagateComposedEvents: true
+                                }
+
+                            }
+
+                            Label {
+                                id: userName
+
+                                text: TimelineManager.escapeEmoji(modelData.userName)
+                                color: TimelineManager.userColor(modelData.userId, colors.window)
+                                textFormat: Text.RichText
+
+                                MouseArea {
+                                    anchors.fill: parent
+                                    Layout.alignment: Qt.AlignHCenter
+                                    onClicked: chat.model.openUserProfile(modelData.userId)
+                                    cursorShape: Qt.PointingHandCursor
+                                    propagateComposedEvents: true
+                                }
+
+                            }
+
+                        }
+
+                    }
+
+                }
+
+                ScrollBar.vertical: ScrollBar {
+                    id: scrollbar
+                }
+
+                delegate: Item {
+                    id: wrapper
+
+                    // This would normally be previousSection, but our model's order is inverted.
+                    property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
+                    property Item section
+
+                    anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
+                    width: chat.delegateMaxWidth
+                    height: section ? section.height + timelinerow.height : timelinerow.height
+                    onSectionBoundaryChanged: {
+                        if (sectionBoundary) {
+                            var properties = {
+                                "modelData": model.dump,
+                                "section": ListView.section,
+                                "nextSection": ListView.nextSection
+                            };
+                            section = sectionHeader.createObject(wrapper, properties);
+                        } else {
+                            section.destroy();
+                            section = null;
+                        }
+                    }
+
+                    TimelineRow {
+                        id: timelinerow
+
+                        y: section ? section.y + section.height : 0
+                    }
+
+                    Connections {
+                        function onMovementEnded() {
+                            if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
+                                chat.model.currentIndex = index;
+
+                        }
+
+                        target: chat
+                    }
+
+                }
+
+                footer: BusyIndicator {
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    running: chat.model && chat.model.paginationInProgress
+                    height: 50
+                    width: 50
+                    z: 3
+                }
+
+            }
+
+            Item {
+                id: chatFooter
+
+                implicitHeight: Math.max(fontMetrics.height * 1.2, footerContent.height)
+                Layout.fillWidth: true
+                z: 3
+
+                Column {
+                    id: footerContent
+
+                    anchors.left: parent.left
+                    anchors.right: parent.right
+                    anchors.bottom: parent.bottom
+
+                    Rectangle {
+                        id: typingRect
+
                         anchors.left: parent.left
-                        anchors.leftMargin: 10
                         anchors.right: parent.right
-                        anchors.rightMargin: 10
-                        color: colors.text
-                        text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : ""
-                        textFormat: Text.RichText
+                        color: (chat.model && chat.model.typingUsers.length > 0) ? colors.window : "transparent"
+                        height: typingDisplay.height
+
+                        Label {
+                            id: typingDisplay
+
+                            anchors.left: parent.left
+                            anchors.leftMargin: 10
+                            anchors.right: parent.right
+                            anchors.rightMargin: 10
+                            color: colors.text
+                            text: chat.model ? chat.model.formatTypingUsers(chat.model.typingUsers, colors.window) : ""
+                            textFormat: Text.RichText
+                        }
+
+                    }
+
+                    Rectangle {
+                        id: replyPopup
+
+                        anchors.left: parent.left
+                        anchors.right: parent.right
+                        visible: chat.model && chat.model.reply
+                        // Height of child, plus margins, plus border
+                        height: replyPreview.height + 10
+                        color: colors.base
+
+                        Reply {
+                            id: replyPreview
+
+                            anchors.left: parent.left
+                            anchors.leftMargin: 10
+                            anchors.right: closeReplyButton.left
+                            anchors.rightMargin: 20
+                            anchors.bottom: parent.bottom
+                            modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {
+                            }
+                            userColor: TimelineManager.userColor(modelData.userId, colors.window)
+                        }
+
+                        ImageButton {
+                            id: closeReplyButton
+
+                            anchors.right: parent.right
+                            anchors.rightMargin: 15
+                            anchors.top: replyPreview.top
+                            hoverEnabled: true
+                            width: 16
+                            height: 16
+                            image: ":/icons/icons/ui/remove-symbol.png"
+                            ToolTip.visible: closeReplyButton.hovered
+                            ToolTip.text: qsTr("Close")
+                            onClicked: chat.model.reply = undefined
+                        }
+
                     }
+
                 }
 
-				Rectangle {
-					anchors.left: parent.left
-					anchors.right: parent.right
-
-					id: replyPopup
-
-					visible: chat.model && chat.model.reply
-					// Height of child, plus margins, plus border
-					height: replyPreview.height + 10
-					color: colors.base
-
-
-					Reply {
-						id: replyPreview
-
-						anchors.left: parent.left
-						anchors.leftMargin: 10
-						anchors.right: closeReplyButton.left
-						anchors.rightMargin: 20
-						anchors.bottom: parent.bottom
-
-						modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {}
-						userColor: TimelineManager.userColor(modelData.userId, colors.window)
-					}
-
-					ImageButton {
-						id: closeReplyButton
-
-						anchors.right: parent.right
-						anchors.rightMargin: 15
-						anchors.top: replyPreview.top
-						hoverEnabled: true
-						width: 16
-						height: 16
-
-						image: ":/icons/icons/ui/remove-symbol.png"
-						ToolTip.visible: closeReplyButton.hovered
-						ToolTip.text: qsTr("Close")
-
-						onClicked: chat.model.reply = undefined
-					}
-				}
-			}
-		}
-
-		ActiveCallBar {
-			Layout.fillWidth: true
-			z: 3
-		}
-	}
-	}
+            }
+
+            ActiveCallBar {
+                Layout.fillWidth: true
+                z: 3
+            }
+
+        }
+
+    }
+
+    systemInactive: SystemPalette {
+        colorGroup: SystemPalette.Disabled
+    }
+
 }