Skip to content
Snippets Groups Projects
MessageView.qml 23.6 KiB
Newer Older
// SPDX-FileCopyrightText: Nheko Contributors
Nicolas Werner's avatar
Nicolas Werner committed
// SPDX-License-Identifier: GPL-3.0-or-later

import "./components"
import "./delegates"
import "./emoji"
import "./dialogs"
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
Item {
    id: chatRoot

    property int availableWidth: width
Nicolas Werner's avatar
Nicolas Werner committed
    property int padding: Nheko.paddingMedium
    property string searchString: ""
    property Room roommodel: room
    // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
    Connections {
        function onHideMenu() {
            replyContextMenuC.close();
Nicolas Werner's avatar
Nicolas Werner committed

    ScrollBar {
        id: scrollbar
Nicolas Werner's avatar
Nicolas Werner committed

        anchors.bottom: parent.bottom
Nicolas Werner's avatar
Nicolas Werner committed
        anchors.right: parent.right
        anchors.top: parent.top
        parent: chat.parent
Malte E's avatar
Malte E committed
    ListView {
        id: chat
Nicolas Werner's avatar
Nicolas Werner committed
        property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - (scrollbar.interactive ? scrollbar.width : 0)
        readonly property alias filteringInProgress: filteredTimeline.filteringInProgress

Nicolas Werner's avatar
Nicolas Werner committed
        ScrollBar.vertical: scrollbar
        anchors.fill: parent
        anchors.rightMargin: scrollbar.interactive ? scrollbar.width : 0
Malte E's avatar
Malte E committed
        // 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
        displayMarginBeginning: height / 4
        displayMarginEnd: height / 4
Nicolas Werner's avatar
Nicolas Werner committed
        model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
Malte E's avatar
Malte E committed
        //pixelAligned: true
        spacing: 2
        verticalLayoutDirection: ListView.BottomToTop
Nicolas Werner's avatar
Nicolas Werner committed

        Component {
            id: defaultMessageStyle

            TimelineDefaultMessageStyle {
                messageActions: messageActionsC
                messageContextMenu: messageContextMenuC
                replyContextMenu: replyContextMenuC
                scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
            }
Nicolas Werner's avatar
Nicolas Werner committed
        }
Nicolas Werner's avatar
Nicolas Werner committed
        Component {
            id: bubbleMessageStyle

            TimelineBubbleMessageStyle {
                messageActions: messageActionsC
                messageContextMenu: messageContextMenuC
                replyContextMenu: replyContextMenuC
Nicolas Werner's avatar
Nicolas Werner committed
                scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
            }
        }
Nicolas Werner's avatar
Nicolas Werner committed
        delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle
Nicolas Werner's avatar
Nicolas Werner committed
        footer: Item {
            anchors.horizontalCenter: parent.horizontalCenter
            anchors.margins: Nheko.paddingLarge
            // hacky, but works
            height: loadingSpinner.height + 2 * Nheko.paddingLarge
            visible: (room && room.paginationInProgress) || chat.filteringInProgress

            Spinner {
                id: loadingSpinner

                anchors.centerIn: parent
                anchors.margins: Nheko.paddingLarge
                foreground: palette.mid
                running: (room && room.paginationInProgress) || chat.filteringInProgress
                z: 3
            }
        }

        Window.onActiveChanged: readTimer.running = Window.active
Malte E's avatar
Malte E committed
        onCountChanged: {
            // Mark timeline as read
Nicolas Werner's avatar
Nicolas Werner committed
            if (atYEnd && room)
                model.currentIndex = 0;
Malte E's avatar
Malte E committed
        }
Nicolas Werner's avatar
Nicolas Werner committed
        TimelineFilter {
            id: filteredTimeline
Nicolas Werner's avatar
Nicolas Werner committed
            filterByContent: chatRoot.searchString
            filterByThread: room ? room.thread : ""
            source: room
        }
        Control {
Malte E's avatar
Malte E committed
            property Item attached: null
            // use comma to update on scroll
Nicolas Werner's avatar
Nicolas Werner committed
            property alias model: row.model
Nicolas Werner's avatar
Nicolas Werner committed
            padding: Nheko.paddingSmall
            visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered)
Malte E's avatar
Malte E committed
            z: 10
Nicolas Werner's avatar
Nicolas Werner committed
            parent: chat.contentItem
            anchors.bottom: attached?.top
            anchors.right: attached?.right

            background: Rectangle {
                border.color: palette.buttonText
                border.width: 1
Nicolas Werner's avatar
Nicolas Werner committed
                color: palette.window
                radius: padding
Malte E's avatar
Malte E committed
            }
            contentItem: RowLayout {
Malte E's avatar
Malte E committed
                id: row
Malte E's avatar
Malte E committed
                property var model
Malte E's avatar
Malte E committed
                Repeater {
                    model: Settings.recentReactions
                    visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false

                    delegate: AbstractButton {
                        id: button
                        property color buttonTextColor: palette.buttonText
Nicolas Werner's avatar
Nicolas Werner committed
                        property color highlightColor: palette.highlight
                        required property string modelData
                        property bool showImage: modelData.startsWith("mxc://")

                        //Layout.preferredHeight: fontMetrics.height
                        Layout.alignment: Qt.AlignBottom
                        focusPolicy: Qt.NoFocus
                        height: showImage ? 16 : buttonText.implicitHeight
                        implicitHeight: showImage ? 16 : buttonText.implicitHeight
Nicolas Werner's avatar
Nicolas Werner committed
                        implicitWidth: showImage ? 16 : buttonText.implicitWidth
                        width: showImage ? 16 : buttonText.implicitWidth

                        onClicked: {
                            room.input.reaction(row.model.eventId, modelData);
                            TimelineManager.focusMessageInput();
                        }

                        Label {
                            id: buttonText

                            anchors.centerIn: parent
                            color: button.hovered ? button.highlightColor : button.buttonTextColor
                            font.family: Settings.emojiFont
                            horizontalAlignment: Text.AlignHCenter
Nicolas Werner's avatar
Nicolas Werner committed
                            padding: 0
                            text: button.modelData
                            verticalAlignment: Text.AlignVCenter
                            visible: !button.showImage
                        }
                        Image {
                            id: buttonImg

                            // Workaround, can't get icon.source working for now...
                            anchors.fill: parent
Nicolas Werner's avatar
Nicolas Werner committed
                            fillMode: Image.PreserveAspectFit
                            source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : ""
                            sourceSize.height: button.height
                            sourceSize.width: button.width
                        }
                        NhekoCursorShape {
                            anchors.fill: parent
                            cursorShape: Qt.PointingHandCursor
                        }
                        Ripple {
                            color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
                        }
Malte E's avatar
Malte E committed
                }
                ImageButton {
Nicolas Werner's avatar
Nicolas Werner committed
                    ToolTip.delay: Nheko.tooltipDelay
                    ToolTip.text: qsTr("Edit")
                    ToolTip.visible: hovered
                    buttonTextColor: palette.buttonText
Malte E's avatar
Malte E committed
                    hoverEnabled: true
                    image: ":/icons/icons/ui/edit.svg"
Nicolas Werner's avatar
Nicolas Werner committed
                    visible: !!row.model && row.model.isEditable
                    width: 16

Malte E's avatar
Malte E committed
                    onClicked: {
Nicolas Werner's avatar
Nicolas Werner committed
                        if (row.model.isEditable)
                            room.edit = row.model.eventId;
Malte E's avatar
Malte E committed
                }
                ImageButton {
                    id: reactButton

                    ToolTip.delay: Nheko.tooltipDelay
                    ToolTip.text: qsTr("React")
Nicolas Werner's avatar
Nicolas Werner committed
                    ToolTip.visible: hovered
                    hoverEnabled: true
                    image: ":/icons/icons/ui/smile-add.svg"
                    visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
                    width: 16
Nicolas Werner's avatar
Nicolas Werner committed
                    onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function (plaintext, markdown) {
                            var event_id = row.model ? row.model.eventId : "";
                            room.input.reaction(event_id, plaintext);
                            TimelineManager.focusMessageInput();
                        })
                }
Nicolas Werner's avatar
Nicolas Werner committed
                ImageButton {
                    ToolTip.delay: Nheko.tooltipDelay
                    ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread")
Nicolas Werner's avatar
Nicolas Werner committed
                    ToolTip.visible: hovered
                    hoverEnabled: true
                    image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg"
                    visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
                    width: 16

                    onClicked: room.thread = (row.model.threadId || row.model.eventId)
Malte E's avatar
Malte E committed
                ImageButton {
                    ToolTip.delay: Nheko.tooltipDelay
                    ToolTip.text: qsTr("Reply")
Nicolas Werner's avatar
Nicolas Werner committed
                    ToolTip.visible: hovered
                    hoverEnabled: true
                    image: ":/icons/icons/ui/reply.svg"
                    visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
                    width: 16

                    onClicked: room.reply = row.model.eventId
Malte E's avatar
Malte E committed
                }
Nicolas Werner's avatar
Nicolas Werner committed
                    ToolTip.delay: Nheko.tooltipDelay
                    ToolTip.text: qsTr("Go to message")
                    ToolTip.visible: hovered
                    buttonTextColor: palette.buttonText
                    hoverEnabled: true
                    image: ":/icons/icons/ui/go-to.svg"
Nicolas Werner's avatar
Nicolas Werner committed
                    visible: !!row.model && filteredTimeline.filterByContent
                    width: 16

                    onClicked: {
                        topBar.searchString = "";
                        room.showEvent(row.model.eventId);
                    }
                }
Malte E's avatar
Malte E committed
                ImageButton {
                    id: optionsButton
Malte E's avatar
Malte E committed
                    ToolTip.delay: Nheko.tooltipDelay
                    ToolTip.text: qsTr("Options")
Nicolas Werner's avatar
Nicolas Werner committed
                    ToolTip.visible: hovered
                    hoverEnabled: true
                    image: ":/icons/icons/ui/options.svg"
                    width: 16

                    onClicked: messageContextMenuC.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
Malte E's avatar
Malte E committed
        }
        Shortcut {
            sequence: StandardKey.MoveToPreviousPage
Nicolas Werner's avatar
Nicolas Werner committed

Malte E's avatar
Malte E committed
            onActivated: {
                chat.contentY = chat.contentY - chat.height * 0.9;
Malte E's avatar
Malte E committed
                chat.returnToBounds();
Malte E's avatar
Malte E committed
        }
        Shortcut {
            sequence: StandardKey.MoveToNextPage
Nicolas Werner's avatar
Nicolas Werner committed

Malte E's avatar
Malte E committed
            onActivated: {
                chat.contentY = chat.contentY + chat.height * 0.9;
Malte E's avatar
Malte E committed
                chat.returnToBounds();
Malte E's avatar
Malte E committed
        }
        Shortcut {
            sequence: StandardKey.Cancel
Nicolas Werner's avatar
Nicolas Werner committed

Malte E's avatar
Malte E committed
            onActivated: {
Nicolas Werner's avatar
Nicolas Werner committed
                if (room.input.uploads.length > 0)
                    room.input.declineUploads();
Nicolas Werner's avatar
Nicolas Werner committed
                else if (room.reply)
                    room.reply = undefined;
                else if (room.edit)
                    room.edit = undefined;
Nicolas Werner's avatar
Nicolas Werner committed
                else
Nicolas Werner's avatar
Nicolas Werner committed
                    room.thread = undefined;
                TimelineManager.focusMessageInput();
Malte E's avatar
Malte E committed
        }
Nicolas Werner's avatar
Nicolas Werner committed
        // These shortcuts use the room timeline because switching to threads and out is annoying otherwise.
        // Better solution welcome.
Malte E's avatar
Malte E committed
        Shortcut {
            sequence: "Alt+Up"
Nicolas Werner's avatar
Nicolas Werner committed

Nicolas Werner's avatar
Nicolas Werner committed
            onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0)
Malte E's avatar
Malte E committed
        }
        Shortcut {
            sequence: "Alt+Down"
Nicolas Werner's avatar
Nicolas Werner committed

Malte E's avatar
Malte E committed
            onActivated: {
Nicolas Werner's avatar
Nicolas Werner committed
                var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1;
                room.reply = idx >= 0 ? room.indexToId(idx) : null;
Malte E's avatar
Malte E committed
        }
        Shortcut {
            sequence: "Alt+F"
Nicolas Werner's avatar
Nicolas Werner committed

Malte E's avatar
Malte E committed
            onActivated: {
                if (room.reply) {
Malte E's avatar
Malte E committed
                    var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
                    forwardMess.setMessageEventId(room.reply);
Malte E's avatar
Malte E committed
                    forwardMess.open();
                    room.reply = null;
Nicolas Werner's avatar
Nicolas Werner committed
                    timelineRoot.destroyOnClose(forwardMess);
Malte E's avatar
Malte E committed
        }
        Shortcut {
            sequence: "Ctrl+E"
Nicolas Werner's avatar
Nicolas Werner committed

Malte E's avatar
Malte E committed
            onActivated: {
                room.edit = room.reply;
Malte E's avatar
Malte E committed
        }
        Timer {
            id: readTimer
Nicolas Werner's avatar
Nicolas Werner committed
            interval: 1000

Malte E's avatar
Malte E committed
            // force current read index to update
            onTriggered: {
Nicolas Werner's avatar
Nicolas Werner committed
                    room.setCurrentIndex(room.currentIndex);
Malte E's avatar
Malte E committed
        }

        property string eventId
        property int eventType
        property bool isEditable
Nicolas Werner's avatar
Nicolas Werner committed
        property bool isEncrypted
        property bool isSender
Nicolas Werner's avatar
Nicolas Werner committed
        property string link
        property string text
        property string threadId
Nicolas Werner's avatar
Nicolas Werner committed
        function show(eventId_, threadId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
            eventId = eventId_;
Nicolas Werner's avatar
Nicolas Werner committed
            threadId = threadId_;
            eventType = eventType_;
            isEncrypted = isEncrypted_;
            isEditable = isEditable_;
            isSender = isSender_;
            if (text_)
Nicolas Werner's avatar
Nicolas Werner committed
                text = text_;
Nicolas Werner's avatar
Nicolas Werner committed
                text = "";
Nicolas Werner's avatar
Nicolas Werner committed
                link = link_;
Nicolas Werner's avatar
Nicolas Werner committed
                link = "";
Nicolas Werner's avatar
Nicolas Werner committed
                open(showAt_);
Nicolas Werner's avatar
Nicolas Werner committed
                open();
        Component {
            id: removeReason
Nicolas Werner's avatar
Nicolas Werner committed

            InputDialog {
                id: removeReasonDialog

                property string eventId

                prompt: qsTr("Enter reason for removal or hit enter for no reason:")
Nicolas Werner's avatar
Nicolas Werner committed
                title: qsTr("Reason for removal")

                onAccepted: function (text) {
                    room.redactEvent(eventId, text);
                }
            }
        }
        Platform.MenuItem {
Nicolas Werner's avatar
Nicolas Werner committed
            enabled: visible
            text: qsTr("Go to &message")
            visible: filteredTimeline.filterByContent

            onTriggered: function () {
                topBar.searchString = "";
Nicolas Werner's avatar
Nicolas Werner committed
        }
        Platform.MenuItem {
            enabled: visible
            text: qsTr("&Copy")
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: Clipboard.text = messageContextMenuC.text
        }
        Platform.MenuItem {
            enabled: visible
            text: qsTr("Copy &link location")
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: Clipboard.text = messageContextMenuC.link
        }
        Platform.MenuItem {
            id: reactionOption

            text: qsTr("Re&act")
Nicolas Werner's avatar
Nicolas Werner committed
            visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
Nicolas Werner's avatar
Nicolas Werner committed
            onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) {
                    room.input.reaction(messageContextMenuC.eventId, plaintext);
Nicolas Werner's avatar
Nicolas Werner committed
                    TimelineManager.focusMessageInput();
                })
        }
        Platform.MenuItem {
            text: qsTr("Repl&y")
Nicolas Werner's avatar
Nicolas Werner committed
            visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false

            onTriggered: room.reply = (messageContextMenuC.eventId)
        }
        Platform.MenuItem {
            enabled: visible
            text: qsTr("&Edit")
            visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: room.edit = (messageContextMenuC.eventId)
Nicolas Werner's avatar
Nicolas Werner committed
        }
        Platform.MenuItem {
            enabled: visible
            text: qsTr("&Thread")
Nicolas Werner's avatar
Nicolas Werner committed
            visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)

            onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId)
Nicolas Werner's avatar
Nicolas Werner committed
        Platform.MenuItem {
            enabled: visible
            text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
Nicolas Werner's avatar
Nicolas Werner committed
            visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)

            onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId)
        Platform.MenuItem {
Nicolas Werner's avatar
Nicolas Werner committed
            text: qsTr("&Read receipts")
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: room.showReadReceipts(messageContextMenuC.eventId)
            text: qsTr("&Forward")
            visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker || messageContextMenuC.eventType == MtxEvent.TextMessage || messageContextMenuC.eventType == MtxEvent.LocationMessage || messageContextMenuC.eventType == MtxEvent.EmoteMessage || messageContextMenuC.eventType == MtxEvent.NoticeMessage
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: {
                var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
                forwardMess.setMessageEventId(messageContextMenuC.eventId);
                forwardMess.open();
Nicolas Werner's avatar
Nicolas Werner committed
                timelineRoot.destroyOnClose(forwardMess);
            text: qsTr("&Mark as read")
        }
        Platform.MenuItem {
            text: qsTr("View raw message")
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: room.viewRawMessage(messageContextMenuC.eventId)
        }
        Platform.MenuItem {
            enabled: visible
            text: qsTr("View decrypted raw message")
Nicolas Werner's avatar
Nicolas Werner committed
            // TODO(Nico): Fix this still being iterated over, when using keyboard to select options
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId)
            text: qsTr("Remo&ve message")
            visible: (room ? room.permissions.canRedact() : false) || messageContextMenuC.isSender
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: function () {
                var dialog = removeReason.createObject(timelineRoot);
                dialog.show();
                dialog.forceActiveFocus();
                timelineRoot.destroyOnClose(dialog);
            }
        }
        Platform.MenuItem {
            enabled: visible
            text: qsTr("&Save as")
            visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: room.saveMedia(messageContextMenuC.eventId)
        }
        Platform.MenuItem {
            enabled: visible
            text: qsTr("&Open in external program")
            visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: room.openMedia(messageContextMenuC.eventId)
        }
        Platform.MenuItem {
            enabled: visible
            text: qsTr("Copy link to eve&nt")
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId)
        }
    }
    Component {
        id: forwardCompleterComponent

        ForwardCompleter {
        }
    }
    Platform.Menu {
        id: replyContextMenuC
Malte E's avatar
Malte E committed
        property string eventId
Nicolas Werner's avatar
Nicolas Werner committed
        property string link
        property string text
Malte E's avatar
Malte E committed
        function show(text_, link_, eventId_) {
            text = text_;
            link = link_;
Malte E's avatar
Malte E committed
            eventId = eventId_;
            open();
        }

        Platform.MenuItem {
            enabled: visible
            text: qsTr("&Copy")
            visible: replyContextMenuC.text
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: Clipboard.text = replyContextMenuC.text
        }
        Platform.MenuItem {
            enabled: visible
            text: qsTr("Copy &link location")
            visible: replyContextMenuC.link
Nicolas Werner's avatar
Nicolas Werner committed

            onTriggered: Clipboard.text = replyContextMenuC.link
        }
        Platform.MenuItem {
            enabled: visible
Nicolas Werner's avatar
Nicolas Werner committed
            text: qsTr("&Go to quoted message")
Nicolas Werner's avatar
Nicolas Werner committed
            visible: true

            onTriggered: room.showEvent(replyContextMenuC.eventId)
Malte E's avatar
Malte E committed
    RoundButton {
        id: toEndButton
Nicolas Werner's avatar
Nicolas Werner committed

Malte E's avatar
Malte E committed
        property int fullWidth: 40
Nicolas Werner's avatar
Nicolas Werner committed

Malte E's avatar
Malte E committed
        flat: true
Nicolas Werner's avatar
Nicolas Werner committed
        height: width
        hoverEnabled: true
Nicolas Werner's avatar
Nicolas Werner committed
        radius: width / 2
        width: 0

        background: Rectangle {
            border.color: toEndButton.hovered ? palette.highlight : palette.buttonText
Nicolas Werner's avatar
Nicolas Werner committed
            color: toEndButton.down ? palette.highlight : palette.button
            opacity: enabled ? 1 : 0.3
            radius: toEndButton.radius
        }
        states: [
            State {
                name: ""
Nicolas Werner's avatar
Nicolas Werner committed

                PropertyChanges {
                    target: toEndButton
                    width: 0
                }
            },
            State {
                name: "shown"
                when: !chat.atYEnd
Nicolas Werner's avatar
Nicolas Werner committed

                PropertyChanges {
                    target: toEndButton
                    width: toEndButton.fullWidth
                }
            }
        ]
        transitions: Transition {
            from: ""
            reversible: true
Nicolas Werner's avatar
Nicolas Werner committed
            to: "shown"
Malte E's avatar
Malte E committed

            SequentialAnimation {
Nicolas Werner's avatar
Nicolas Werner committed
                PauseAnimation {
                    duration: 500
                }
Malte E's avatar
Malte E committed
                PropertyAnimation {
                    duration: 200
Nicolas Werner's avatar
Nicolas Werner committed
                    easing.type: Easing.InOutQuad
                    properties: "width"
                    target: toEndButton
Malte E's avatar
Malte E committed
                }
            }
Nicolas Werner's avatar
Nicolas Werner committed

        onClicked: function () {
            chat.positionViewAtBeginning();
            TimelineManager.focusMessageInput();
        }

        anchors {
            bottom: parent.bottom
            bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2
            right: scrollbar.left
            rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2
        }
        Image {
            id: buttonImg

            anchors.fill: parent
            anchors.margins: Nheko.paddingMedium
            fillMode: Image.PreserveAspectFit
            source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? palette.highlightedText : palette.buttonText)
        }
Malte E's avatar
Malte E committed
    }