mirror of https://github.com/Nheko-Reborn/nheko
parent
fc7a1bdfba
commit
a86e364d1a
@ -0,0 +1,323 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors |
||||
// |
||||
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
||||
import "./components" |
||||
import "./delegates" |
||||
import "./emoji" |
||||
import "./ui" |
||||
import "./dialogs" |
||||
import Qt.labs.platform 1.1 as Platform |
||||
import QtQuick |
||||
import QtQuick.Controls |
||||
import QtQuick.Layouts |
||||
import QtQuick.Window |
||||
import im.nheko |
||||
|
||||
TimelineEvent { |
||||
id: wrapper |
||||
ListView.delayRemove: true |
||||
width: chat.delegateMaxWidth |
||||
height: Math.max((section.item?.height ?? 0) + 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 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) + 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 |
||||
color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent" |
||||
|
||||
// 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 |
||||
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 int metadataWidth: 100 |
||||
property int metadataHeight: 20 |
||||
|
||||
property bool fitsMetadata: ((wrapper.main?.width ?? 0) + wrapper.mainInset + metadata.width) < wrapper.maxWidth |
||||
|
||||
implicitWidth: Math.max((wrapper.reply?.width ?? 0) + wrapper.replyInset, (wrapper.main?.width ?? 0) + wrapper.mainInset + (fitsMetadata ? metadata.width : 0)) |
||||
implicitHeight: contentColumn.implicitHeight + (fitsMetadata ? 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 / 5) + 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) |
||||
} |
||||
} |
||||
} |
||||
|
||||
data: [replyRow, wrapper.main] |
||||
} |
||||
} |
||||
|
||||
padding: 4 |
||||
background: Rectangle { |
||||
color: !wrapper.isStateEvent ? Qt.tint(palette.base, Qt.hsla(messageBubble.userColor.hslHue, 0.5, messageBubble.userColor.hslLightness, 0.2)) : "transparent" |
||||
radius: 4 |
||||
border.color: Nheko.theme.red |
||||
border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0 |
||||
} |
||||
} |
||||
}, |
||||
Reactions { |
||||
id: reactionRow |
||||
|
||||
eventId: wrapper.eventId |
||||
layoutDirection: row.bubbleOnRight ? 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 |
||||
} |
||||
} |
||||
] |
||||
} |
||||
|
@ -0,0 +1,98 @@ |
||||
// SPDX-FileCopyrightText: Nheko Contributors |
||||
// |
||||
// SPDX-License-Identifier: GPL-3.0-or-later |
||||
|
||||
import "./components" |
||||
import "./delegates" |
||||
import "./emoji" |
||||
import "./ui" |
||||
import "./dialogs" |
||||
import Qt.labs.platform 1.1 as Platform |
||||
import QtQuick |
||||
import QtQuick.Controls |
||||
import QtQuick.Layouts |
||||
import QtQuick.Window |
||||
import im.nheko |
||||
|
||||
RowLayout { |
||||
id: metadata |
||||
|
||||
property int iconSize: Math.floor(fontMetrics.ascent * scaling) |
||||
required property double scaling |
||||
|
||||
required property string eventId |
||||
required property int status |
||||
required property int trustlevel |
||||
required property bool isEdited |
||||
required property bool isEncrypted |
||||
required property string threadId |
||||
required property date timestamp |
||||
required property Room room |
||||
|
||||
spacing: 2 |
||||
|
||||
StatusIndicator { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter |
||||
eventId: metadata.eventId |
||||
height: parent.iconSize |
||||
status: metadata.status |
||||
width: parent.iconSize |
||||
} |
||||
Image { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: qsTr("Edited") |
||||
ToolTip.visible: editHovered.hovered |
||||
height: parent.iconSize |
||||
source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((metadata.eventId == metadata.room.edit) ? palette.highlight : palette.buttonText) |
||||
sourceSize.height: parent.iconSize * Screen.devicePixelRatio |
||||
sourceSize.width: parent.iconSize * Screen.devicePixelRatio |
||||
visible: metadata.isEdited || metadata.eventId == metadata.room.edit |
||||
width: parent.iconSize |
||||
|
||||
HoverHandler { |
||||
id: editHovered |
||||
|
||||
} |
||||
} |
||||
ImageButton { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: qsTr("Part of a thread") |
||||
ToolTip.visible: hovered |
||||
buttonTextColor: TimelineManager.userColor(metadata.threadId, palette.base) |
||||
height: parent.iconSize |
||||
image: ":/icons/icons/ui/thread.svg" |
||||
visible: metadata.threadId |
||||
width: parent.iconSize |
||||
|
||||
onClicked: metadata.room.thread = threadId |
||||
} |
||||
EncryptionIndicator { |
||||
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter |
||||
encrypted: metadata.isEncrypted |
||||
height: parent.iconSize |
||||
sourceSize.height: parent.iconSize * Screen.devicePixelRatio |
||||
sourceSize.width: parent.iconSize * Screen.devicePixelRatio |
||||
trust: metadata.trustlevel |
||||
visible: metadata.room.isEncrypted |
||||
width: parent.iconSize |
||||
} |
||||
Label { |
||||
id: ts |
||||
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop |
||||
Layout.preferredWidth: implicitWidth |
||||
ToolTip.delay: Nheko.tooltipDelay |
||||
ToolTip.text: Qt.formatDateTime(metadata.timestamp, Qt.DefaultLocaleLongDate) |
||||
ToolTip.visible: ma.hovered |
||||
color: palette.inactive.text |
||||
font.pointSize: fontMetrics.font.pointSize * parent.scaling |
||||
text: metadata.timestamp.toLocaleTimeString(Locale.ShortFormat) |
||||
|
||||
HoverHandler { |
||||
id: ma |
||||
|
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue