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

import "./delegates"
import "./emoji"
import Qt.labs.platform 1.1 as Platform
import QtGraphicalEffects 1.0
import QtQuick 2.12
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.2
import QtQuick.Window 2.2
    palette: Nheko.colors
    ScrollBar.horizontal.visible: false

        property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < parent.availableWidth) ? Settings.timelineMaxWidth : parent.availableWidth) - parent.padding * 2
        boundsBehavior: Flickable.StopAtBounds
        pixelAligned: true
        spacing: 4
        verticalLayoutDirection: ListView.BottomToTop
        onCountChanged: {
            // Mark timeline as read
            if (atYEnd)
                model.currentIndex = 0;
        Rectangle {
            //closePolicy: Popup.NoAutoClose

            id: messageActions

            property Item attached: null
            property alias model: row.model
            // use comma to update on scroll
            property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null
            readonly property int padding: 4

            visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || messageActionHover.hovered)
            x: attached ? attachedPos.x : 0
            y: attached ? attachedPos.y : 0
            z: 10
            height: row.implicitHeight + padding * 2
            width: row.implicitWidth + padding * 2
            color: Nheko.colors.window
            border.color: Nheko.colors.buttonText
            border.width: 1
            radius: padding

            HoverHandler {
                id: messageActionHover
                grabPermissions: PointerHandler.CanTakeOverFromAnything
            }

            Row {
                id: row

                property var model

                anchors.centerIn: parent
                spacing: messageActions.padding

                ImageButton {
                    id: editButton

                    visible: !!row.model && row.model.isEditable
                    buttonTextColor: Nheko.colors.buttonText
                    width: 16
                    hoverEnabled: true
                    image: ":/icons/icons/ui/edit.png"
                    ToolTip.visible: hovered
Nicolas Werner's avatar
Nicolas Werner committed
                    ToolTip.text: qsTr("Edit")
                    onClicked: {
                        if (row.model.isEditable)
                            chat.model.editAction(row.model.id);

                    }
                }

                EmojiButton {
                    id: reactButton

                    visible: chat.model ? chat.model.permissions.canSend(MtxEvent.Reaction) : false
                    width: 16
                    hoverEnabled: true
                    ToolTip.visible: hovered
                    ToolTip.text: qsTr("React")
                    emojiPicker: emojiPopup
                    event_id: row.model ? row.model.id : ""
                }

                ImageButton {
                    id: replyButton

                    visible: chat.model ? chat.model.permissions.canSend(MtxEvent.TextMessage) : false
                    width: 16
                    hoverEnabled: true
                    image: ":/icons/icons/ui/mail-reply.png"
                    ToolTip.visible: hovered
                    ToolTip.text: qsTr("Reply")
                    onClicked: chat.model.replyAction(row.model.id)
                }

                ImageButton {
                    id: optionsButton

                    width: 16
                    hoverEnabled: true
                    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)
        ScrollHelper {
            flickable: parent
            anchors.fill: parent
            enabled: !Settings.mobileMode
        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: {
                if (chat.model.reply)
                    chat.model.reply = undefined;
                else
                    chat.model.edit = 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) : null;
            }
        }

        Shortcut {
            sequence: "Alt+F"
            onActivated: {
                if (chat.model.reply) {
                    var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
                    forwardMess.setMessageEventId(chat.model.reply);
                    forwardMess.open();
                    chat.model.reply = null;
                }
        Shortcut {
            sequence: "Ctrl+E"
            onActivated: {
                chat.model.edit = chat.model.reply;
            }
        }
        Connections {
            target: TimelineManager
            onFocusChanged: readTimer.running = TimelineManager.isWindowFocused
        }
            // force current read index to update
            onTriggered: chat.model.setCurrentIndex(chat.model.currentIndex)
            interval: 1000
        }
        Component {
            id: sectionHeader
            Column {
                topPadding: 4
                bottomPadding: 4
                visible: modelData && (modelData.previousMessageUserId !== modelData.userId || modelData.previousMessageDay !== modelData.day)
                width: parentWidth
                height: ((modelData && modelData.previousMessageDay !== modelData.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) : ""
                    color: Nheko.colors.text
                    height: Math.round(fontMetrics.height * 1.4)
                    width: contentWidth * 1.2
                    horizontalAlignment: Text.AlignHCenter
                    verticalAlignment: Text.AlignVCenter

                    background: Rectangle {
                        radius: parent.height / 2
                        color: Nheko.colors.window
trilene's avatar
trilene committed

                Row {
                    height: userName.height
                    spacing: 8

                    Avatar {
                        id: messageUserAvatar

                        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)
                        ToolTip.visible: avatarHover.hovered
                        ToolTip.text: userid

                        HoverHandler {
                            id: avatarHover
                        }

                    Connections {
                        target: chat.model
                        onRoomAvatarUrlChanged: {
                            messageUserAvatar.url = modelData ? chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/") : "";
                        }
                        onScrollToIndex: chat.positionViewAtIndex(index, ListView.Visible)
                    Label {
                        id: userName

                        text: modelData ? TimelineManager.escapeEmoji(modelData.userName) : ""
                        color: TimelineManager.userColor(modelData ? modelData.userId : "", Nheko.colors.window)
                        textFormat: Text.RichText
                        ToolTip.visible: displayNameHover.hovered
                        ToolTip.text: modelData ? modelData.userId : ""
                        TapHandler {
                            onSingleTapped: chat.model.openUserProfile(modelData.userId)
                        }

                        CursorShape {
                            anchors.fill: parent
                            cursorShape: Qt.PointingHandCursor
                        }
                        HoverHandler {
                            id: displayNameHover
                        }

                        color: Nheko.colors.buttonText
                        text: modelData ? TimelineManager.userStatus(modelData.userId) : ""
                        textFormat: Text.PlainText
                        elide: Text.ElideRight
                        width: chat.delegateMaxWidth - parent.spacing * 2 - userName.implicitWidth - Nheko.avatarSize
        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)

            anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
            width: chat.delegateMaxWidth
            height: section ? section.height + timelinerow.height : timelinerow.height
            Rectangle {
                id: scrollHighlight

                opacity: 0
                visible: true
                anchors.fill: timelinerow
                color: Nheko.colors.highlight

                states: State {
                    name: "revealed"
                    when: wrapper.scrolledToThis
                }

                transitions: Transition {
                    from: ""
                    to: "revealed"

                    SequentialAnimation {
                        PropertyAnimation {
                            target: scrollHighlight
                            properties: "opacity"
                            easing.type: Easing.InOutQuad
                            from: 0
                            to: 1
                            duration: 500
                        }

                        PropertyAnimation {
                            target: scrollHighlight
                            properties: "opacity"
                            easing.type: Easing.InOutQuad
                            from: 1
                            to: 0
                            duration: 500
                        }

                        ScriptAction {
                            script: chat.model.eventShown()
                        }

                    }

                }

            }

                property var modelData: model
                property int parentWidth: parent.width
                active: model.previousMessageUserId !== undefined && model.previousMessageUserId !== model.userId || model.previousMessageDay !== model.day
                //asynchronous: true
                sourceComponent: sectionHeader
                visible: status == Loader.Ready
            }
            TimelineRow {
                id: timelinerow
                property alias hovered: hoverHandler.hovered

                y: section.visible && section.active ? section.y + section.height : 0

                HoverHandler {
                    id: hoverHandler

                    enabled: !Settings.mobileMode
                    onHoveredChanged: {
                        if (hovered) {
                            if (!messageActionHover.hovered) {
                                messageActions.attached = timelinerow;
                                messageActions.model = model;
                            }
                        }
                    }
                }

            Connections {
                target: chat
                onMovementEnded: {
                    if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
                        chat.model.currentIndex = index;
            anchors.horizontalCenter: parent.horizontalCenter
            running: chat.model && chat.model.paginationInProgress
            foreground: Nheko.colors.mid
            visible: chat.model && chat.model.paginationInProgress
    Platform.Menu {
        id: messageContextMenu

        property string eventId
        property string link
        property string text
        property int eventType
        property bool isEncrypted
        property bool isEditable
        property bool isSender

        function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
            eventId = eventId_;
            eventType = eventType_;
            isEncrypted = isEncrypted_;
            isEditable = isEditable_;
            isSender = isSender_;
            if (text_)
                text = text_;
            else
                text = "";
            if (link_)
                link = link_;
            else
                link = "";
            if (showAt_)
                open(showAt_);
            else
                open();
        }

        Platform.MenuItem {
            visible: messageContextMenu.text
            enabled: visible
            text: qsTr("&Copy")
            onTriggered: Clipboard.text = messageContextMenu.text
        }

        Platform.MenuItem {
            visible: messageContextMenu.link
            enabled: visible
            text: qsTr("Copy &link location")
            onTriggered: Clipboard.text = messageContextMenu.link
        }

        Platform.MenuItem {
            id: reactionOption

            visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
            text: qsTr("Re&act")
            onTriggered: emojiPopup.show(null, function(emoji) {
                room.input.reaction(messageContextMenu.eventId, emoji);
            })
        }

        Platform.MenuItem {
            visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
            text: qsTr("Repl&y")
            onTriggered: room.replyAction(messageContextMenu.eventId)
        }

        Platform.MenuItem {
            visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
            enabled: visible
            text: qsTr("&Edit")
            onTriggered: room.editAction(messageContextMenu.eventId)
        }

        Platform.MenuItem {
            text: qsTr("Read receip&ts")
            onTriggered: room.readReceiptsAction(messageContextMenu.eventId)
        }

        Platform.MenuItem {
            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage
            text: qsTr("&Forward")
            onTriggered: {
                var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
                forwardMess.setMessageEventId(messageContextMenu.eventId);
                forwardMess.open();
            }
        }

        Platform.MenuItem {
            text: qsTr("&Mark as read")
        }

        Platform.MenuItem {
            text: qsTr("View raw message")
            onTriggered: room.viewRawMessage(messageContextMenu.eventId)
        }

        Platform.MenuItem {
            // TODO(Nico): Fix this still being iterated over, when using keyboard to select options
            visible: messageContextMenu.isEncrypted
            enabled: visible
            text: qsTr("View decrypted raw message")
            onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId)
        }

        Platform.MenuItem {
            visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender
            text: qsTr("Remo&ve message")
            onTriggered: room.redactEvent(messageContextMenu.eventId)
        }

        Platform.MenuItem {
            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
            enabled: visible
            text: qsTr("&Save as")
            onTriggered: room.saveMedia(messageContextMenu.eventId)
        }

        Platform.MenuItem {
            visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker
            enabled: visible
            text: qsTr("&Open in external program")
            onTriggered: room.openMedia(messageContextMenu.eventId)
        }

        Platform.MenuItem {
            visible: messageContextMenu.eventId
            enabled: visible
            text: qsTr("Copy link to eve&nt")
            onTriggered: room.copyLinkToEvent(messageContextMenu.eventId)
        }

    }

    Component {
        id: forwardCompleterComponent

        ForwardCompleter {
        }

    }