summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--resources/qml/MessageView.qml11
-rw-r--r--resources/qml/TimelineBubbleMessageStyle.qml323
-rw-r--r--resources/qml/TimelineDefaultMessageStyle.qml82
-rw-r--r--resources/qml/TimelineMetadata.qml98
5 files changed, 445 insertions, 71 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index cbe2b20e..a426f5d2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -703,6 +703,8 @@ set(QML_SOURCES
         resources/qml/RoomList.qml
 				resources/qml/TimelineSectionHeader.qml
 				resources/qml/TimelineDefaultMessageStyle.qml
+				resources/qml/TimelineBubbleMessageStyle.qml
+				resources/qml/TimelineMetadata.qml
         resources/qml/TimelineView.qml
         resources/qml/Avatar.qml
         resources/qml/Completer.qml
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 5ea73fb5..ab8a3ee8 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -68,8 +68,17 @@ Item {
                 scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
             }
         }
+        Component {
+            id: bubbleMessageStyle
+
+            TimelineBubbleMessageStyle {
+                messageActions: messageActionsC
+                messageContextMenu: messageContextMenuC
+                scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
+            }
+        }
 
-        delegate: defaultMessageStyle
+        delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle
         footer: Item {
             anchors.horizontalCenter: parent.horizontalCenter
             anchors.margins: Nheko.paddingLarge
diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml
new file mode 100644
index 00000000..c6c1aede
--- /dev/null
+++ b/resources/qml/TimelineBubbleMessageStyle.qml
@@ -0,0 +1,323 @@
+// SPDX-FileCopyrightText: Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "./components"
+import "./delegates"
+import "./emoji"
+import "./ui"
+import "./dialogs"
+import Qt.labs.platform 1.1 as Platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Window
+import im.nheko
+
+TimelineEvent {
+    id: wrapper
+    ListView.delayRemove: true
+    width: chat.delegateMaxWidth
+    height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10)
+    anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter
+    //room: chatRoot.roommodel
+
+    required property var day
+    required property bool isSender
+    required property int index
+    property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day)
+    property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent)
+    property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId)
+
+    required property date timestamp
+    required property string userId
+    required property string userName
+    required property string threadId
+    required property int userPowerlevel
+    required property bool isEdited
+    required property bool isEncrypted
+    required property var reactions
+    required property int status
+    required property int trustlevel
+    required property int notificationlevel
+    required property int type
+    required property bool isEditable
+
+    required property QtObject messageContextMenu
+    required property Item messageActions
+
+    property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header
+
+    property alias hovered: messageHover.hovered
+    property bool scrolledToThis: false
+
+    mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4
+    replyInset: mainInset + 4 + Nheko.paddingSmall
+
+    property int bubbleMargin: 40
+
+    maxWidth: chat.delegateMaxWidth - avatarMargin - bubbleMargin
+
+    data: [
+        Loader {
+            id: section
+
+            active: wrapper.previousMessageUserId !== wrapper.userId || wrapper.previousMessageDay !== wrapper.day || wrapper.previousMessageIsStateEvent !== wrapper.isStateEvent
+            //asynchronous: true
+            sourceComponent: TimelineSectionHeader {
+                day: wrapper.day
+                isSender: wrapper.isSender
+                isStateEvent: wrapper.isStateEvent
+                parentWidth: wrapper.width
+                previousMessageDay: wrapper.previousMessageDay
+                previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
+                previousMessageUserId: wrapper.previousMessageUserId
+                timestamp: wrapper.timestamp
+                userId: wrapper.userId
+                userName: wrapper.userName
+                userPowerlevel: wrapper.userPowerlevel
+            }
+            visible: status == Loader.Ready
+            z: 4
+        }, 
+        Rectangle {
+            anchors.fill: gridContainer
+            color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent"
+
+            // this looks better without margins
+            TapHandler {
+                acceptedButtons: Qt.RightButton
+                acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
+                gesturePolicy: TapHandler.ReleaseWithinBounds
+
+                onSingleTapped: messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText)
+            }
+        },
+        Rectangle {
+            id: scrollHighlight
+            anchors.fill: gridContainer
+
+            color: palette.highlight
+            enabled: false
+            opacity: 0
+            visible: true
+            z: 1
+
+            states: State {
+                name: "revealed"
+                when: wrapper.scrolledToThis
+            }
+            transitions: Transition {
+                from: ""
+                to: "revealed"
+
+                SequentialAnimation {
+                    PropertyAnimation {
+                        duration: 500
+                        easing.type: Easing.InOutQuad
+                        from: 0
+                        properties: "opacity"
+                        target: scrollHighlight
+                        to: 1
+                    }
+                    PropertyAnimation {
+                        duration: 500
+                        easing.type: Easing.InOutQuad
+                        from: 1
+                        properties: "opacity"
+                        target: scrollHighlight
+                        to: 0
+                    }
+                    ScriptAction {
+                        script: wrapper.room.eventShown()
+                    }
+                }
+            }
+        },
+        Item {
+            id: gridContainer
+
+            width: wrapper.width - wrapper.avatarMargin
+            implicitHeight: messageBubble.implicitHeight
+            x: wrapper.avatarMargin
+            y: section.visible && section.active ? section.y + section.height : 0
+
+            HoverHandler {
+                id: messageHover
+                blocking: false
+                onHoveredChanged: () => {
+                    if (!Settings.mobileMode && hovered) {
+                        if (!messageActions.hovered) {
+                            messageActions.model = wrapper;
+                            messageActions.attached = wrapper;
+                            messageActions.anchors.bottomMargin = -gridContainer.y
+                            //messageActions.anchors.rightMargin = metadata.width
+                        }
+                    }
+                }
+
+            }
+
+
+            AbstractButton {
+                id: messageBubble
+
+                anchors.left: (wrapper.isStateEvent || wrapper.isSender) ? undefined : parent.left
+                anchors.right: (wrapper.isStateEvent || !wrapper.isSender) ? undefined : parent.right
+                anchors.horizontalCenter: wrapper.isStateEvent ? parent.horizontalCenter : undefined
+
+                property color userColor: TimelineManager.userColor(wrapper.main?.userId ?? '', palette.base)
+
+                contentItem: Item {
+                    id: contentPlacementContainer
+
+                    property int metadataWidth: 100
+                    property int metadataHeight: 20
+
+                    property bool fitsMetadata: ((wrapper.main?.width ?? 0) + wrapper.mainInset + metadata.width) < wrapper.maxWidth
+
+                    implicitWidth: Math.max((wrapper.reply?.width ?? 0) + wrapper.replyInset, (wrapper.main?.width ?? 0) + wrapper.mainInset + (fitsMetadata ? metadata.width : 0))
+                    implicitHeight: contentColumn.implicitHeight + (fitsMetadata ? 0 : metadata.height)
+
+                    TimelineMetadata {
+                        id: metadata
+
+                        scaling: 0.75
+
+                        anchors.right: parent.right
+                        anchors.bottom: parent.bottom
+
+                        visible: !wrapper.isStateEvent
+
+                        eventId: wrapper.eventId
+                        status: wrapper.status
+                        trustlevel: wrapper.trustlevel
+                        isEdited: wrapper.isEdited
+                        isEncrypted: wrapper.isEncrypted
+                        threadId: wrapper.threadId
+                        timestamp: wrapper.timestamp
+                        room: wrapper.room
+                    }
+
+                    Column {
+                        id: contentColumn
+
+                        anchors.left: parent.left
+                        anchors.right: parent.right
+
+                        AbstractButton {
+                            id: replyRow
+                            visible: wrapper.reply
+
+                            height: replyLine.height
+                            anchors.left: parent.left
+                            anchors.right: parent.right
+
+                            property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base)
+
+                            clip: true
+
+                            NhekoCursorShape {
+                                anchors.fill: parent
+                                cursorShape: Qt.PointingHandCursor
+                            }
+
+                            contentItem: Row {
+                                id: replyRowLay
+
+                                spacing: Nheko.paddingSmall
+
+                                Rectangle {
+                                    id: replyLine
+                                    height: Math.min( wrapper.reply?.height, timelineView.height / 5) + Nheko.paddingSmall + replyUserButton.height
+                                    color: replyRow.userColor
+                                    width: 4
+                                }
+
+                                Column {
+                                    spacing: 0
+
+                                    id: replyCol
+
+                                    AbstractButton {
+                                        id: replyUserButton
+
+                                        contentItem: Label {
+                                            id: userName_
+                                            text: wrapper.reply?.userName ?? ''
+                                            color: replyRow.userColor
+                                            textFormat: Text.RichText
+                                            width: wrapper.maxWidth
+                                            //elideWidth: wrapper.maxWidth
+                                        }
+                                        onClicked: wrapper.room.openUserProfile(wrapper.reply?.userId)
+                                    }
+                                    data: [
+                                        replyUserButton,
+                                        wrapper.reply,
+                                    ]
+                                }
+                            }
+
+                            background: Rectangle {
+                                //width: replyRow.implicitContentWidth
+                                color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1))
+                            }
+
+                            onClicked: {
+                                let link = wrapper.reply.hoveredLink
+                                if (link) {
+                                    Nheko.openLink(link)
+                                } else {
+                                    console.log("Scrolling to "+wrapper.replyTo);
+                                    wrapper.room.showEvent(wrapper.replyTo)
+                                }
+                            }
+                        }
+
+                        data: [replyRow, wrapper.main]
+                    }
+                }
+
+                padding: 4
+                background: Rectangle {
+                    color: !wrapper.isStateEvent ? Qt.tint(palette.base, Qt.hsla(messageBubble.userColor.hslHue, 0.5, messageBubble.userColor.hslLightness, 0.2)) : "transparent"
+                    radius: 4
+                    border.color: Nheko.theme.red
+                    border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0
+                }
+            }
+        },
+        Reactions {
+            id: reactionRow
+
+            eventId: wrapper.eventId
+            layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight
+            reactions: wrapper.reactions
+            width: wrapper.width - wrapper.avatarMargin
+            x: wrapper.avatarMargin
+
+            anchors {
+                //left: row.bubbleOnRight ? undefined : row.left
+                //right: row.bubbleOnRight ? row.right : undefined
+                top: gridContainer.bottom
+                topMargin: -4
+            }
+        },
+        Rectangle {
+            id: unreadRow
+
+            color: palette.highlight
+            height: visible ? 3 : 0
+            visible: (wrapper.index > 0 && (wrapper.room.fullyReadEventId == wrapper.eventId))
+
+            anchors {
+                left: parent.left
+                right: parent.right
+                top: reactionRow.bottom
+                topMargin: 5
+            }
+        }
+    ]
+}
+
diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml
index 8beaa8f0..f4906208 100644
--- a/resources/qml/TimelineDefaultMessageStyle.qml
+++ b/resources/qml/TimelineDefaultMessageStyle.qml
@@ -51,7 +51,7 @@ TimelineEvent {
     property alias hovered: messageHover.hovered
     property bool scrolledToThis: false
 
-    mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4
+    mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0)
     replyInset: mainInset + 4 + Nheko.paddingSmall
 
     maxWidth: chat.delegateMaxWidth - avatarMargin - metadata.width
@@ -269,82 +269,24 @@ TimelineEvent {
             }
 
         },
-            RowLayout {
+            TimelineMetadata {
                 id: metadata
 
-                property int iconSize: Math.floor(fontMetrics.ascent * scaling)
-                property double scaling: Settings.bubbles ? 0.75 : 1
+                scaling: 1
 
                 anchors.right: parent.right
                 y: section.visible && section.active ? section.y + section.height : 0
 
-                spacing: 2
-                visible: !isStateEvent
+                visible: !wrapper.isStateEvent
 
-                StatusIndicator {
-                    Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-                    eventId: wrapper.eventId
-                    height: parent.iconSize
-                    status: wrapper.status
-                    width: parent.iconSize
-                }
-                Image {
-                    Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("Edited")
-                    ToolTip.visible: editHovered.hovered
-                    height: parent.iconSize
-                    source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((wrapper.eventId == wrapper.room.edit) ? palette.highlight : palette.buttonText)
-                    sourceSize.height: parent.iconSize * Screen.devicePixelRatio
-                    sourceSize.width: parent.iconSize * Screen.devicePixelRatio
-                    visible: wrapper.isEdited || wrapper.eventId == wrapper.room.edit
-                    width: parent.iconSize
-
-                    HoverHandler {
-                        id: editHovered
-
-                    }
-                }
-                ImageButton {
-                    Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: qsTr("Part of a thread")
-                    ToolTip.visible: hovered
-                    buttonTextColor: TimelineManager.userColor(wrapper.threadId, palette.base)
-                    height: parent.iconSize
-                    image: ":/icons/icons/ui/thread.svg"
-                    visible: wrapper.threadId
-                    width: parent.iconSize
-
-                    onClicked: wrapper.room.thread = threadId
-                }
-                EncryptionIndicator {
-                    Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
-                    encrypted: wrapper.isEncrypted
-                    height: parent.iconSize
-                    sourceSize.height: parent.iconSize * Screen.devicePixelRatio
-                    sourceSize.width: parent.iconSize * Screen.devicePixelRatio
-                    trust: wrapper.trustlevel
-                    visible: wrapper.room.isEncrypted
-                    width: parent.iconSize
-                }
-                Label {
-                    id: ts
-
-                    Layout.alignment: Qt.AlignRight | Qt.AlignTop
-                    Layout.preferredWidth: implicitWidth
-                    ToolTip.delay: Nheko.tooltipDelay
-                    ToolTip.text: Qt.formatDateTime(wrapper.timestamp, Qt.DefaultLocaleLongDate)
-                    ToolTip.visible: ma.hovered
-                    color: palette.inactive.text
-                    font.pointSize: fontMetrics.font.pointSize * parent.scaling
-                    text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat)
-
-                    HoverHandler {
-                        id: ma
-
-                    }
-                }
+                eventId: wrapper.eventId
+                status: wrapper.status
+                trustlevel: wrapper.trustlevel
+                isEdited: wrapper.isEdited
+                isEncrypted: wrapper.isEncrypted
+                threadId: wrapper.threadId
+                timestamp: wrapper.timestamp
+                room: wrapper.room
             },
         Reactions {
             id: reactionRow
diff --git a/resources/qml/TimelineMetadata.qml b/resources/qml/TimelineMetadata.qml
new file mode 100644
index 00000000..53282fc5
--- /dev/null
+++ b/resources/qml/TimelineMetadata.qml
@@ -0,0 +1,98 @@
+// SPDX-FileCopyrightText: Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "./components"
+import "./delegates"
+import "./emoji"
+import "./ui"
+import "./dialogs"
+import Qt.labs.platform 1.1 as Platform
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtQuick.Window
+import im.nheko
+
+RowLayout {
+    id: metadata
+
+    property int iconSize: Math.floor(fontMetrics.ascent * scaling)
+    required property double scaling
+
+    required property string eventId
+    required property int status
+    required property int trustlevel
+    required property bool isEdited
+    required property bool isEncrypted
+    required property string threadId
+    required property date timestamp
+    required property Room room
+
+    spacing: 2
+
+    StatusIndicator {
+        Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+        eventId: metadata.eventId
+        height: parent.iconSize
+        status: metadata.status
+        width: parent.iconSize
+    }
+    Image {
+        Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+        ToolTip.delay: Nheko.tooltipDelay
+        ToolTip.text: qsTr("Edited")
+        ToolTip.visible: editHovered.hovered
+        height: parent.iconSize
+        source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((metadata.eventId == metadata.room.edit) ? palette.highlight : palette.buttonText)
+        sourceSize.height: parent.iconSize * Screen.devicePixelRatio
+        sourceSize.width: parent.iconSize * Screen.devicePixelRatio
+        visible: metadata.isEdited || metadata.eventId == metadata.room.edit
+        width: parent.iconSize
+
+        HoverHandler {
+            id: editHovered
+
+        }
+    }
+    ImageButton {
+        Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+        ToolTip.delay: Nheko.tooltipDelay
+        ToolTip.text: qsTr("Part of a thread")
+        ToolTip.visible: hovered
+        buttonTextColor: TimelineManager.userColor(metadata.threadId, palette.base)
+        height: parent.iconSize
+        image: ":/icons/icons/ui/thread.svg"
+        visible: metadata.threadId
+        width: parent.iconSize
+
+        onClicked: metadata.room.thread = threadId
+    }
+    EncryptionIndicator {
+        Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
+        encrypted: metadata.isEncrypted
+        height: parent.iconSize
+        sourceSize.height: parent.iconSize * Screen.devicePixelRatio
+        sourceSize.width: parent.iconSize * Screen.devicePixelRatio
+        trust: metadata.trustlevel
+        visible: metadata.room.isEncrypted
+        width: parent.iconSize
+    }
+    Label {
+        id: ts
+
+        Layout.alignment: Qt.AlignRight | Qt.AlignTop
+        Layout.preferredWidth: implicitWidth
+        ToolTip.delay: Nheko.tooltipDelay
+        ToolTip.text: Qt.formatDateTime(metadata.timestamp, Qt.DefaultLocaleLongDate)
+        ToolTip.visible: ma.hovered
+        color: palette.inactive.text
+        font.pointSize: fontMetrics.font.pointSize * parent.scaling
+        text: metadata.timestamp.toLocaleTimeString(Locale.ShortFormat)
+
+        HoverHandler {
+            id: ma
+
+        }
+    }
+}