Skip to content
Snippets Groups Projects
TimelineView.qml 19.5 KiB
Newer Older
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 QtQuick.Window 2.2
import im.nheko.EmojiModel 1.0
    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 {
            visible: TimelineManager.timeline != null
            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
                    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()
                        }

                    }

                        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)

                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.right: parent.right
                        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
                        }

            }

            ActiveCallBar {
                Layout.fillWidth: true
                z: 3
            }

        }

    }

    systemInactive: SystemPalette {
        colorGroup: SystemPalette.Disabled
    }