mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-22 11:00:48 +03:00
368 lines
16 KiB
QML
368 lines
16 KiB
QML
// SPDX-FileCopyrightText: Nheko Contributors
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Window
|
|
import im.nheko
|
|
|
|
TimelineEvent {
|
|
id: wrapper
|
|
ListView.delayRemove: true
|
|
width: chat.delegateMaxWidth
|
|
// We return a larger size for any item but the most bottom one, if it isn't initialized yet, since otherwise Qt will create way too many items.
|
|
// If we did that also for the first item, it would mess with the scroll location a bit, so we don't do it for that item.
|
|
height: Math.max((section.item?.height ?? 0) + ((gridContainer.implicitHeight < 1 && index != 0) ? 100 : 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 QtObject replyContextMenu
|
|
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
|
|
|
|
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
|
|
property color threadColor: TimelineManager.userColor(wrapper.threadId, palette.base)
|
|
property color threadBackgroundColor: wrapper.threadId ? Qt.tint(palette.base, Qt.hsla(threadColor.hslHue, 0.7, threadColor.hslLightness, 0.1)) : "transparent"
|
|
color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : threadBackgroundColor
|
|
|
|
// 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 // qmllint disable Quick.anchor-combinations
|
|
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 bool fitsMetadata: ((wrapper.main?.width ?? 0) + wrapper.mainInset + metadata.width) < wrapper.maxWidth
|
|
|
|
// This doesnt work because of tables. They might have content in the top of the cell, while the background reaches to the bottom. Maybe using the textDocument we could do more?
|
|
// property bool fitsMetadataInside: wrapper.main?.positionAt ? (wrapper.main.positionAt(wrapper.main.width, wrapper.main.height - 4) == wrapper.main.positionAt(wrapper.main.width - metadata.width, wrapper.main.height - 4)) : false
|
|
property bool fitsMetadataInside: false
|
|
|
|
implicitWidth: Math.max((wrapper.reply?.width ?? 0) + wrapper.replyInset, (wrapper.main?.width ?? 0) + wrapper.mainInset + ((fitsMetadata && !fitsMetadataInside) ? metadata.width : 0))
|
|
implicitHeight: contentColumn.implicitHeight + ((fitsMetadata || fitsMetadataInside) ? 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 / 10) + 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)
|
|
}
|
|
}
|
|
onPressAndHold: wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(pressX-replyLine.width - Nheko.paddingSmall, pressY - replyUserButton.implicitHeight) : "", wrapper.replyTo)
|
|
TapHandler {
|
|
acceptedButtons: Qt.RightButton
|
|
onSingleTapped: (eventPoint) => wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(eventPoint.position.x-replyLine.width - Nheko.paddingSmall, eventPoint.position.y - replyUserButton.implicitHeight) : "", wrapper.replyTo)
|
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
|
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
|
}
|
|
}
|
|
|
|
data: [replyRow, wrapper.main]
|
|
}
|
|
|
|
|
|
}
|
|
|
|
padding: wrapper.isStateEvent ? 0 : 4
|
|
background: Rectangle {
|
|
color: !wrapper.isStateEvent ? Qt.tint(palette.base, Qt.hsla(messageBubble.userColor.hslHue, wrapper.hovered ? 0.8 : 0.5, messageBubble.userColor.hslLightness, 0.2)) : "transparent"
|
|
radius: 4
|
|
border.color: Nheko.theme.red
|
|
border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0
|
|
}
|
|
}
|
|
|
|
DragHandler {
|
|
id: replyDragHandler
|
|
yAxis.enabled: false
|
|
xAxis.enabled: true
|
|
xAxis.minimum: wrapper.avatarMargin - 100
|
|
xAxis.maximum: wrapper.avatarMargin
|
|
onActiveChanged: {
|
|
if (!replyDragHandler.active) {
|
|
if (replyDragHandler.xAxis.minimum <= replyDragHandler.xAxis.activeValue + 1) {
|
|
wrapper.room.reply = wrapper.eventId
|
|
}
|
|
gridContainer.x = wrapper.avatarMargin;
|
|
}
|
|
}
|
|
}
|
|
|
|
TapHandler {
|
|
onDoubleTapped: wrapper.room.reply = wrapper.eventId
|
|
}
|
|
|
|
},
|
|
Item {
|
|
// We need this item to grab events, that otherwise would go to the TextArea in the main item. If we don't have this, it would trigger a right click menu on KDE...
|
|
// https://invent.kde.org/frameworks/qqc2-desktop-style/-/blob/9d71fe874186009f76d392e203d9fa25a49f8be7/org.kde.desktop/TextArea.qml#L55
|
|
|
|
anchors.fill: gridContainer
|
|
anchors.topMargin: replyRow.height
|
|
TapHandler {
|
|
|
|
acceptedButtons: Qt.RightButton
|
|
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad
|
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
|
|
|
onSingleTapped: (event) => {
|
|
messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText);
|
|
}
|
|
}
|
|
},
|
|
Reactions {
|
|
id: reactionRow
|
|
|
|
eventId: wrapper.eventId
|
|
layoutDirection: (!wrapper.isStateEvent && wrapper.isSender) ? 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
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|