// Copyright Mirage authors & contributors // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 import QtQuick.Layouts 1.12 import "../../../Base" import "../../.." HRowLayout { id: eventContent readonly property var mentions: JSON.parse(model.mentions) readonly property string mentionsCSS: { const lines = [] for (const [name, link] of mentions) { if (! link.match(/^https?:\/\/matrix.to\/#\/@.+/)) continue lines.push( `.mention[data-mention='${utils.escapeHtml(name)}'] ` + `{ color: ${utils.nameColor(name)} }` ) } return "" } readonly property string senderText: asOneLine || onRight || combine ? "" : ( `<${compact ? "span" : "div"} class='sender'>` + utils.coloredNameHtml(model.sender_name, model.sender_id) + (compact ? ": " : "") + (compact ? "" : "") ) property string contentText: utils.processedEventText(model) readonly property string timeText: utils.formatTime(model.date, false) readonly property string stateText: `` + ` ⧗` : // U+29D7 model.read_by_count ? `color="${theme.chat.message.readCounter}"> ⦿ ` + model.read_by_count : // U+29BF ">" ) + "" readonly property bool pureMedia: ! contentText && linksRepeater.count readonly property bool hoveredSelectable: contentHover.hovered readonly property string hoveredLink: linksRepeater.lastHovered && linksRepeater.lastHovered.hovered ? linksRepeater.lastHovered.mediaUrl : contentLabel.hoveredLink readonly property alias contentLabel: contentLabel readonly property int xOffset: onRight ? Math.min( contentColumn.width - contentLabel.paintedWidth - contentLabel.leftPadding - contentLabel.rightPadding, contentColumn.width - linksRepeater.widestChild - ( pureMedia ? 0 : contentLabel.leftPadding + contentLabel.rightPadding ), ) : 0 readonly property int maxMessageWidth: contentText.includes("
") || contentText.includes("") ?
        -1 :
        window.settings.Chat.max_messages_line_length < 0 ?
        -1 :
        Math.ceil(
            mainUI.fontMetrics.averageCharacterWidth *
            window.settings.Chat.max_messages_line_length
        )

    readonly property alias selectedText: contentLabel.selectedPlainText

    spacing: theme.chat.message.horizontalSpacing
    layoutDirection: onRight ? Qt.RightToLeft: Qt.LeftToRight

    Item {
        id: avatarWrapper
        visible: ! onRight
        opacity: combine ? 0 : 1

        Layout.alignment: Qt.AlignTop

        Layout.preferredWidth:
            compact ?
            theme.chat.message.collapsedAvatarSize :
            theme.chat.message.avatarSize

        Layout.preferredHeight:
            combine ?
            1 :

            compact || (
                asOneLine &&
                nextModel &&
                eventList.canCombine(model, nextModel)
            ) ?
            theme.chat.message.collapsedAvatarSize :

            theme.chat.message.avatarSize

        HUserAvatar {
            id: avatar
            clientUserId: chat.userId
            userId: model.sender_id
            displayName: model.sender_name
            mxc: model.sender_avatar
            width: parent.width
            height: combine ? 1 : parent.Layout.preferredWidth
            radius: theme.chat.message.avatarRadius
        }
    }

    HColumnLayout {
        id: contentColumn

        Layout.fillWidth: true
        Layout.alignment: Qt.AlignVCenter

        HSelectableLabel {
            id: contentLabel
            visible: ! pureMedia
            enableLinkActivation: ! eventList.selectedCount

            selectByMouse:
                eventList.selectedCount <= 1 &&
                eventDelegate.checked &&
                textSelectionBlocker.point.scenePosition === Qt.point(0, 0)

            topPadding: theme.chat.message.verticalSpacing
            bottomPadding: topPadding
            leftPadding: eventContent.spacing
            rightPadding: leftPadding

            color: model.event_type === "RoomMessageNotice" ?
                   theme.chat.message.noticeBody :
                   theme.chat.message.body

            font.italic: model.event_type === "RoomMessageEmote"
            wrapMode: TextEdit.Wrap
            textFormat: Text.RichText
            text:
                // CSS
                theme.chat.message.styleInclude + mentionsCSS +

                // Sender name & message body
                (
                    compact && contentText.match(/^\s*<(p|h[1-6])>/) ?
                    contentText.replace(
                        /(^\s*<(p|h[1-6])>)/, "$1" + senderText,
                    ) :
                    senderText + contentText
                ) +

                // Time
                // For some reason, if there's only one space,
                // times will be on their own lines most of the time.
                "  " +
                `` +
                timeText +
                "" +

                stateText

            transform: Translate { x: xOffset }

            Layout.maximumWidth: eventContent.maxMessageWidth
            Layout.fillWidth: true

            onSelectedTextChanged: if (selectedPlainText) {
                eventList.delegateWithSelectedText = model.id
                eventList.selectedText             = selectedPlainText
            } else if (eventList.delegateWithSelectedText === model.id) {
                eventList.delegateWithSelectedText = ""
                eventList.selectedText             = ""
            }

            Connections {
                target: eventList
                onCheckedChanged: contentLabel.deselect()
                onDelegateWithSelectedTextChanged: {
                    if (eventList.delegateWithSelectedText !== model.id)
                        contentLabel.deselect()
                }
            }

            HoverHandler { id: contentHover }

            PointHandler {
                id: mousePointHandler

                property bool checkedNow: false

                acceptedButtons: Qt.LeftButton
                acceptedModifiers: Qt.NoModifier
                acceptedPointerTypes:
                    PointerDevice.GenericPointer | PointerDevice.Eraser

                onActiveChanged: {
                    if (active &&
                            ! eventDelegate.checked &&
                            (! parent.hoveredLink ||
                            ! parent.enableLinkActivation)) {

                        eventList.check(model.index)
                        checkedNow = true
                    }

                    if (! active && eventDelegate.checked) {
                        checkedNow ?
                        checkedNow = false :
                        eventList.uncheck(model.index)
                    }
                }
            }

            PointHandler {
                id: mouseShiftPointHandler
                acceptedButtons: Qt.LeftButton
                acceptedModifiers: Qt.ShiftModifier
                acceptedPointerTypes:
                    PointerDevice.GenericPointer | PointerDevice.Eraser

                onActiveChanged: {
                    if (active &&
                            ! eventDelegate.checked &&
                            (! parent.hoveredLink ||
                            ! parent.enableLinkActivation)) {

                        eventList.checkFromLastToHere(model.index)
                    }
                }
            }

            TapHandler {
                id: touchTapHandler
                acceptedButtons: Qt.LeftButton
                acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen
                onTapped:
                    if (! parent.hoveredLink || ! parent.enableLinkActivation)
                        eventDelegate.toggleChecked()
            }

            TapHandler {
                id: textSelectionBlocker
                acceptedPointerTypes: PointerDevice.Finger | PointerDevice.Pen
            }

            HToolTip {
                readonly property bool keyboardShow:
                    eventList.showFocusedSeenTooltips &&
                    eventList.currentIndex === model.index &&
                    model.read_by_count > 0

                instant: keyboardShow
                visible:
                    eventContent.hoveredLink === "#state-text" || keyboardShow

                label.textFormat: HLabel.StyledText
                text: {
                    if (! visible) return ""

                    const members =
                        ModelStore.get(chat.userId, chat.roomId, "members")

                    const readBy = Object.entries(
                        JSON.parse(model.last_read_by)
                    ).sort((a, b) => a[1] - b[1])  // sort by values (dates)

                    const lines  = []

                    for (const [userId, epoch] of readBy) {
                        const member = members.find(userId)

                        const by  = utils.coloredNameHtml(
                            member ? member.display_name: userId, userId,
                        )
                        const at = utils.formatRelativeTime(
                            new Date(epoch) - model.date,
                        )
                        lines.push(qsTr("Seen by %1 %2 after").arg(by).arg(at))
                    }

                    return lines.join("
") } } Rectangle { id: contentBackground width: Math.max( parent.paintedWidth + parent.leftPadding + parent.rightPadding, linksRepeater.summedWidth + (pureMedia ? 0 : parent.leftPadding + parent.rightPadding), ) height: contentColumn.height radius: theme.chat.message.radius z: -100 color: eventDelegate.checked && ! contentLabel.selectedPlainText && ! mousePointHandler.active && ! mouseShiftPointHandler.active ? theme.chat.message.checkedBackground : isOwn? theme.chat.message.ownBackground : theme.chat.message.background Behavior on color { HColorAnimation {} } Rectangle { visible: model.event_type === "RoomMessageNotice" // y: parent.height / 2 - height / 2 width: theme.chat.message.noticeLineWidth height: parent.height radius: parent.radius color: utils.nameColor( model.sender_name || model.sender_id.substring(1), ) } } } HRepeater { id: linksRepeater property EventMediaLoader lastHovered: null model: { const links = JSON.parse(eventDelegate.currentModel.links) if (eventDelegate.currentModel.media_url) links.push(eventDelegate.currentModel.media_url) return links } EventMediaLoader { singleMediaInfo: eventDelegate.currentModel mediaUrl: modelData showSender: pureMedia ? senderText : "" showDate: pureMedia ? timeText : "" showLocalEcho: pureMedia && ( singleMediaInfo.is_local_echo || singleMediaInfo.read_by_count ) ? stateText : "" transform: Translate { x: xOffset } onHoveredChanged: if (hovered) linksRepeater.lastHovered = this Layout.bottomMargin: pureMedia ? 0 : contentLabel.bottomPadding Layout.leftMargin: pureMedia ? 0 : eventContent.spacing Layout.rightMargin: pureMedia ? 0 : eventContent.spacing Layout.preferredWidth: item ? item.width : -1 Layout.preferredHeight: item ? item.height : -1 } } } HSpacer {} }