mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-12-05 00:28:49 +03:00
364 lines
14 KiB
QML
364 lines
14 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
|
|
property bool scrolledToThis: false
|
|
|
|
mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0)
|
|
replyInset: mainInset + 4 + Nheko.paddingSmall
|
|
|
|
maxWidth: chat.delegateMaxWidth - avatarMargin - metadata.width
|
|
|
|
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 {
|
|
// this looks better without margins
|
|
anchors.fill: gridContainer
|
|
color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent"
|
|
|
|
// This is partially duplicated by a later handler, however we need this to handle the remaining events around the reply.
|
|
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);
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
},
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Row {
|
|
id: gridContainer
|
|
|
|
width: wrapper.width - wrapper.avatarMargin
|
|
x: wrapper.avatarMargin
|
|
y: section.visible && section.active ? section.y + section.height : 0
|
|
spacing: Nheko.paddingSmall
|
|
|
|
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 {
|
|
ToolTip.delay: Nheko.tooltipDelay
|
|
ToolTip.text: qsTr("Part of a thread")
|
|
ToolTip.visible: hovered
|
|
height: contentColumn.height
|
|
visible: wrapper.threadId
|
|
width: 4
|
|
|
|
onClicked: wrapper.room.thread = wrapper.threadId
|
|
|
|
Rectangle {
|
|
id: threadLine
|
|
|
|
anchors.fill: parent
|
|
color: TimelineManager.userColor(wrapper.threadId, palette.base)
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: stateEventSpacing
|
|
visible: wrapper.isStateEvent
|
|
width: (wrapper.maxWidth - (wrapper.main?.width ?? 0)) / 2
|
|
height: 1
|
|
}
|
|
|
|
Column {
|
|
id: contentColumn
|
|
|
|
AbstractButton {
|
|
id: replyRow
|
|
visible: wrapper.reply
|
|
|
|
height: replyLine.height
|
|
|
|
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,
|
|
]
|
|
}
|
|
|
|
DragHandler {
|
|
id: replyDragHandler
|
|
enabled: !Settings.disableSwipe
|
|
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
|
|
}
|
|
},
|
|
Rectangle {
|
|
anchors.top: gridContainer.top
|
|
anchors.left: gridContainer.left
|
|
anchors.topMargin: -2
|
|
anchors.leftMargin: -2 + (stateEventSpacing.visible ? (stateEventSpacing.width + gridContainer.spacing) : 0)
|
|
color: "transparent"
|
|
border.color: Nheko.theme.red
|
|
border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0
|
|
radius: 4
|
|
height: contentColumn.implicitHeight + 4
|
|
width: contentColumn.implicitWidth + 4 + (wrapper.threadId ? (4 + gridContainer.spacing) : 0)
|
|
},
|
|
TimelineMetadata {
|
|
id: metadata
|
|
|
|
scaling: 1
|
|
|
|
anchors.right: parent.right
|
|
y: section.visible && section.active ? section.y + section.height : 0
|
|
|
|
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
|
|
},
|
|
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
|
|
reactions: wrapper.reactions
|
|
width: wrapper.width - wrapper.avatarMargin
|
|
x: wrapper.avatarMargin
|
|
|
|
anchors {
|
|
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
|
|
}
|
|
}
|
|
]
|
|
}
|