Move message styles to their own files and work around hover not propagating to siblings

This commit is contained in:
Nicolas Werner 2023-09-20 02:17:20 +02:00
parent 43c8e64ed3
commit 184806bf71
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
6 changed files with 530 additions and 467 deletions

View file

@ -701,6 +701,8 @@ set(QML_SOURCES
resources/qml/ChatPage.qml resources/qml/ChatPage.qml
resources/qml/CommunitiesList.qml resources/qml/CommunitiesList.qml
resources/qml/RoomList.qml resources/qml/RoomList.qml
resources/qml/TimelineSectionHeader.qml
resources/qml/TimelineDefaultMessageStyle.qml
resources/qml/TimelineView.qml resources/qml/TimelineView.qml
resources/qml/Avatar.qml resources/qml/Avatar.qml
resources/qml/Completer.qml resources/qml/Completer.qml

View file

@ -25,7 +25,7 @@ Item {
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
Connections { Connections {
function onHideMenu() { function onHideMenu() {
messageContextMenu.close(); messageContextMenuC.close();
replyContextMenu.close(); replyContextMenu.close();
} }
@ -59,293 +59,9 @@ Item {
spacing: 2 spacing: 2
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
delegate: TimelineEvent { delegate: TimelineDefaultMessageStyle {
id: wrapper messageActions: messageActionsC
ListView.delayRemove: true messageContextMenu: messageContextMenuC
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 type
required property bool isEditable
property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header
property alias hovered: messageHover.hovered
data: [
Loader {
id: section
property var day: wrapper.day
property bool isSender: wrapper.isSender
property bool isStateEvent: wrapper.isStateEvent
property int parentWidth: wrapper.width
property var previousMessageDay: wrapper.previousMessageDay
property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
property string previousMessageUserId: wrapper.previousMessageUserId
property date timestamp: wrapper.timestamp
property string userId: wrapper.userId
property string userName: wrapper.userName
property string userPowerlevel: wrapper.userPowerlevel
active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
//asynchronous: true
sourceComponent: sectionHeader
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)
}
},
RowLayout {
id: gridContainer
width: wrapper.width
y: section.visible && section.active ? section.y + section.height : 0
Item {
Layout.preferredWidth: wrapper.avatarMargin
}
AbstractButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Part of a thread")
ToolTip.visible: hovered
Layout.fillHeight: true
visible: wrapper.threadId
Layout.preferredWidth: 4
onClicked: room.thread = wrapper.threadId
Rectangle {
id: threadLine
anchors.fill: parent
color: TimelineManager.userColor(wrapper.threadId, palette.base)
}
}
ColumnLayout {
id: contentColumn
Layout.fillWidth: true
AbstractButton {
id: replyRow
visible: wrapper.reply
Layout.fillWidth: true
Layout.maximumHeight: timelineView.height / 8
Layout.preferredWidth: replyRowLay.implicitWidth
Layout.preferredHeight: replyRowLay.implicitHeight
property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base)
clip: true
contentItem: RowLayout {
id: replyRowLay
anchors.fill: parent
Rectangle {
id: replyLine
Layout.fillHeight: true
color: replyRow.userColor
Layout.preferredWidth: 4
}
ColumnLayout {
spacing: 0
AbstractButton {
id: replyUserButton
Layout.fillWidth: true
contentItem: ElidedLabel {
id: userName_
fullText: wrapper.reply?.userName ?? ''
color: replyRow.userColor
textFormat: Text.RichText
width: parent.width
elideWidth: width
}
onClicked: 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))
}
}
data: [
replyRow, wrapper.main,
]
}
Item {
// spacer to fill width if needed
Layout.fillWidth: true
}
RowLayout {
id: metadata
property int iconSize: Math.floor(fontMetrics.ascent * scaling)
property double scaling: Settings.bubbles ? 0.75 : 1
Layout.alignment: Qt.AlignTop | Qt.AlignRight
Layout.preferredWidth: implicitWidth
spacing: 2
visible: !isStateEvent
StatusIndicator {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
eventId: wrapper.eventId
height: parent.iconSize
status: wrapper.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?" + ((wrapper.eventId == room.edit) ? palette.highlight : palette.buttonText)
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
visible: wrapper.isEdited || wrapper.eventId == 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(wrapper.threadId, palette.base)
height: parent.iconSize
image: ":/icons/icons/ui/thread.svg"
visible: wrapper.threadId
width: parent.iconSize
onClicked: room.thread = threadId
}
EncryptionIndicator {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
encrypted: wrapper.isEncrypted
height: parent.iconSize
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
trust: wrapper.trustlevel
visible: 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(wrapper.timestamp, Qt.DefaultLocaleLongDate)
ToolTip.visible: ma.hovered
color: palette.inactive.text
font.pointSize: fontMetrics.font.pointSize * parent.scaling
text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat)
HoverHandler {
id: ma
}
}
}
},
Item {
id: messageActionsAnchor
anchors.fill: gridContainer
property alias hovered: messageHover.hovered
HoverHandler {
id: messageHover
onHoveredChanged: () => {
if (!Settings.mobileMode && hovered) {
if (!messageActions.hovered) {
messageActions.model = wrapper;
messageActions.attached = wrapper;
messageActions.anchors.bottomMargin = -gridContainer.y
}
}
}
}
},
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 && (room.fullyReadEventId == wrapper.eventId))
anchors {
left: parent.left
right: parent.right
top: reactionRow.bottom
topMargin: 5
}
}
]
} }
footer: Item { footer: Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@ -380,7 +96,7 @@ Item {
source: room source: room
} }
Control { Control {
id: messageActions id: messageActionsC
property Item attached: null property Item attached: null
// use comma to update on scroll // use comma to update on scroll
@ -405,7 +121,7 @@ Item {
property var model property var model
spacing: messageActions.padding spacing: messageActionsC.padding
Repeater { Repeater {
model: Settings.recentReactions model: Settings.recentReactions
@ -542,7 +258,7 @@ Item {
image: ":/icons/icons/ui/options.svg" image: ":/icons/icons/ui/options.svg"
width: 16 width: 16
onClicked: messageContextMenu.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton) onClicked: messageContextMenuC.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
} }
} }
} }
@ -624,152 +340,9 @@ Item {
room.setCurrentIndex(room.currentIndex); room.setCurrentIndex(room.currentIndex);
} }
} }
Component {
id: sectionHeader
Column {
bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3
spacing: 8
topPadding: userName_.visible ? 4 : 0
visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
width: parentWidth
Label {
id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
color: palette.text
height: Math.round(fontMetrics.height * 1.4)
horizontalAlignment: Text.AlignHCenter
text: room ? room.formatDateSeparator(timestamp) : ""
verticalAlignment: Text.AlignVCenter
visible: room && previousMessageDay !== day
width: contentWidth * 1.2
background: Rectangle {
color: palette.window
radius: parent.height / 2
}
}
Row {
id: userInfo
property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width
height: userName_.height
spacing: 8
visible: !isStateEvent && (!isSender || !Settings.bubbles)
Avatar {
id: messageUserAvatar
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: userid
ToolTip.visible: messageUserAvatar.hovered
displayName: userName
height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
userid: userId
width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
onClicked: room.openUserProfile(userId)
}
Connections {
function onRoomAvatarUrlChanged() {
messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/");
}
function onScrollToIndex(index) {
chat.positionViewAtIndex(index, ListView.Center);
}
target: room
}
AbstractButton {
id: userNameButton
PowerlevelIndicator {
id: powerlevelIndicator
anchors.left: parent.left
//anchors.horizontalCenter: parent.horizontalCenter
powerlevel: userPowerlevel
height: fontMetrics.lineSpacing
width: fontMetrics.lineSpacing
sourceSize.width: fontMetrics.lineSpacing
sourceSize.height: fontMetrics.lineSpacing
permissions: room ? room.permissions : null
visible: isAdmin || isModerator
}
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: userId
ToolTip.visible: hovered
leftPadding: powerlevelIndicator.visible ? 16 : 0
leftInset: 0
rightInset: 0
rightPadding: 0
contentItem: Label {
id: userName_
color: TimelineManager.userColor(userId, palette.base)
text: TimelineManager.escapeEmoji(userNameTextMetrics.elidedText)
textFormat: Text.RichText
}
onClicked: room.openUserProfile(userId)
TextMetrics {
id: userNameTextMetrics
elide: Text.ElideRight
elideWidth: userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3)
text: userName
}
NhekoCursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
}
Label {
id: statusMsg
property string userStatus: Presence.userStatus(userId)
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("%1's status message").arg(userName)
ToolTip.visible: statusMsgHoverHandler.hovered
anchors.baseline: userNameButton.baseline
color: palette.buttonText
elide: Text.ElideRight
font.italic: true
font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8)
text: userStatus.replace(/\n/g, " ")
textFormat: Text.PlainText
width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing)
HoverHandler {
id: statusMsgHoverHandler
}
Connections {
function onPresenceChanged(id) {
if (id == userId)
statusMsg.userStatus = Presence.userStatus(userId);
}
target: Presence
}
}
}
}
}
} }
Platform.Menu { Platform.Menu {
id: messageContextMenu id: messageContextMenuC
property string eventId property string eventId
property int eventType property int eventType
@ -824,22 +397,22 @@ Item {
onTriggered: function () { onTriggered: function () {
topBar.searchString = ""; topBar.searchString = "";
room.showEvent(messageContextMenu.eventId); room.showEvent(messageContextMenuC.eventId);
} }
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("&Copy") text: qsTr("&Copy")
visible: messageContextMenu.text visible: messageContextMenuC.text
onTriggered: Clipboard.text = messageContextMenu.text onTriggered: Clipboard.text = messageContextMenuC.text
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("Copy &link location") text: qsTr("Copy &link location")
visible: messageContextMenu.link visible: messageContextMenuC.link
onTriggered: Clipboard.text = messageContextMenu.link onTriggered: Clipboard.text = messageContextMenuC.link
} }
Platform.MenuItem { Platform.MenuItem {
id: reactionOption id: reactionOption
@ -848,7 +421,7 @@ Item {
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) { onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) {
room.input.reaction(messageContextMenu.eventId, plaintext); room.input.reaction(messageContextMenuC.eventId, plaintext);
TimelineManager.focusMessageInput(); TimelineManager.focusMessageInput();
}) })
} }
@ -856,41 +429,41 @@ Item {
text: qsTr("Repl&y") text: qsTr("Repl&y")
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
onTriggered: room.reply = (messageContextMenu.eventId) onTriggered: room.reply = (messageContextMenuC.eventId)
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("&Edit") text: qsTr("&Edit")
visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: room.edit = (messageContextMenu.eventId) onTriggered: room.edit = (messageContextMenuC.eventId)
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("&Thread") text: qsTr("&Thread")
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: room.thread = (messageContextMenu.threadId || messageContextMenu.eventId) onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId)
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? qsTr("Un&pin") : qsTr("&Pin") text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false) visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
onTriggered: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? room.unpin(messageContextMenu.eventId) : room.pin(messageContextMenu.eventId) onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId)
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("&Read receipts") text: qsTr("&Read receipts")
onTriggered: room.showReadReceipts(messageContextMenu.eventId) onTriggered: room.showReadReceipts(messageContextMenuC.eventId)
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("&Forward") text: qsTr("&Forward")
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker || messageContextMenuC.eventType == MtxEvent.TextMessage || messageContextMenuC.eventType == MtxEvent.LocationMessage || messageContextMenuC.eventType == MtxEvent.EmoteMessage || messageContextMenuC.eventType == MtxEvent.NoticeMessage
onTriggered: { onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot); var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenu.eventId); forwardMess.setMessageEventId(messageContextMenuC.eventId);
forwardMess.open(); forwardMess.open();
timelineRoot.destroyOnClose(forwardMess); timelineRoot.destroyOnClose(forwardMess);
} }
@ -901,23 +474,23 @@ Item {
Platform.MenuItem { Platform.MenuItem {
text: qsTr("View raw message") text: qsTr("View raw message")
onTriggered: room.viewRawMessage(messageContextMenu.eventId) onTriggered: room.viewRawMessage(messageContextMenuC.eventId)
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("View decrypted raw message") text: qsTr("View decrypted raw message")
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options // TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenu.isEncrypted visible: messageContextMenuC.isEncrypted
onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId) onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId)
} }
Platform.MenuItem { Platform.MenuItem {
text: qsTr("Remo&ve message") text: qsTr("Remo&ve message")
visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender visible: (room ? room.permissions.canRedact() : false) || messageContextMenuC.isSender
onTriggered: function () { onTriggered: function () {
var dialog = removeReason.createObject(timelineRoot); var dialog = removeReason.createObject(timelineRoot);
dialog.eventId = messageContextMenu.eventId; dialog.eventId = messageContextMenuC.eventId;
dialog.show(); dialog.show();
dialog.forceActiveFocus(); dialog.forceActiveFocus();
timelineRoot.destroyOnClose(dialog); timelineRoot.destroyOnClose(dialog);
@ -926,23 +499,23 @@ Item {
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("&Save as") text: qsTr("&Save as")
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
onTriggered: room.saveMedia(messageContextMenu.eventId) onTriggered: room.saveMedia(messageContextMenuC.eventId)
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("&Open in external program") text: qsTr("&Open in external program")
visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
onTriggered: room.openMedia(messageContextMenu.eventId) onTriggered: room.openMedia(messageContextMenuC.eventId)
} }
Platform.MenuItem { Platform.MenuItem {
enabled: visible enabled: visible
text: qsTr("Copy link to eve&nt") text: qsTr("Copy link to eve&nt")
visible: messageContextMenu.eventId visible: messageContextMenuC.eventId
onTriggered: room.copyLinkToEvent(messageContextMenu.eventId) onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId)
} }
} }
Component { Component {

View file

@ -0,0 +1,320 @@
// 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 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
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)
}
},
RowLayout {
id: gridContainer
width: wrapper.width
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
}
}
}
}
Item {
Layout.preferredWidth: wrapper.avatarMargin
}
AbstractButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Part of a thread")
ToolTip.visible: hovered
Layout.fillHeight: true
visible: wrapper.threadId
Layout.preferredWidth: 4
onClicked: wrapper.room.thread = wrapper.threadId
Rectangle {
id: threadLine
anchors.fill: parent
color: TimelineManager.userColor(wrapper.threadId, palette.base)
}
}
ColumnLayout {
id: contentColumn
Layout.fillWidth: true
AbstractButton {
id: replyRow
visible: wrapper.reply
Layout.fillWidth: true
Layout.maximumHeight: timelineView.height / 8
Layout.preferredWidth: replyRowLay.implicitWidth
Layout.preferredHeight: replyRowLay.implicitHeight
property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base)
clip: true
NhekoCursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
contentItem: RowLayout {
id: replyRowLay
anchors.fill: parent
Rectangle {
id: replyLine
Layout.fillHeight: true
color: replyRow.userColor
Layout.preferredWidth: 4
}
ColumnLayout {
spacing: 0
AbstractButton {
id: replyUserButton
Layout.fillWidth: true
contentItem: ElidedLabel {
id: userName_
fullText: wrapper.reply?.userName ?? ''
color: replyRow.userColor
textFormat: Text.RichText
width: parent.width
elideWidth: width
}
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,
]
}
Item {
// spacer to fill width if needed
Layout.fillWidth: true
}
RowLayout {
id: metadata
property int iconSize: Math.floor(fontMetrics.ascent * scaling)
property double scaling: Settings.bubbles ? 0.75 : 1
Layout.alignment: Qt.AlignTop | Qt.AlignRight
Layout.preferredWidth: implicitWidth
spacing: 2
visible: !isStateEvent
StatusIndicator {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
eventId: wrapper.eventId
height: parent.iconSize
status: wrapper.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?" + ((wrapper.eventId == wrapper.room.edit) ? palette.highlight : palette.buttonText)
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
visible: wrapper.isEdited || wrapper.eventId == wrapper.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(wrapper.threadId, palette.base)
height: parent.iconSize
image: ":/icons/icons/ui/thread.svg"
visible: wrapper.threadId
width: parent.iconSize
onClicked: wrapper.room.thread = threadId
}
EncryptionIndicator {
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
encrypted: wrapper.isEncrypted
height: parent.iconSize
sourceSize.height: parent.iconSize * Screen.devicePixelRatio
sourceSize.width: parent.iconSize * Screen.devicePixelRatio
trust: wrapper.trustlevel
visible: wrapper.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(wrapper.timestamp, Qt.DefaultLocaleLongDate)
ToolTip.visible: ma.hovered
color: palette.inactive.text
font.pointSize: fontMetrics.font.pointSize * parent.scaling
text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat)
HoverHandler {
id: ma
}
}
}
},
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
}
}
]
}

View file

@ -0,0 +1,164 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
import im.nheko 1.0
Column {
required property var day
required property bool isSender
required property bool isStateEvent
required property int parentWidth
required property var previousMessageDay
required property bool previousMessageIsStateEvent
required property string previousMessageUserId
required property date timestamp
required property string userId
required property string userName
required property string userPowerlevel
bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3
spacing: 8
topPadding: userName_.visible ? 4 : 0
visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
width: parentWidth
Label {
id: dateBubble
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
color: palette.text
height: Math.round(fontMetrics.height * 1.4)
horizontalAlignment: Text.AlignHCenter
text: room ? room.formatDateSeparator(timestamp) : ""
verticalAlignment: Text.AlignVCenter
visible: room && previousMessageDay !== day
width: contentWidth * 1.2
background: Rectangle {
color: palette.window
radius: parent.height / 2
}
}
Row {
id: userInfo
property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width
height: userName_.height
spacing: 8
visible: !isStateEvent && (!isSender || !Settings.bubbles)
Avatar {
id: messageUserAvatar
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: userid
ToolTip.visible: messageUserAvatar.hovered
displayName: userName
height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
userid: userId
width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1)
onClicked: room.openUserProfile(userId)
}
Connections {
function onRoomAvatarUrlChanged() {
messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/");
}
function onScrollToIndex(index) {
chat.positionViewAtIndex(index, ListView.Center);
}
target: room
}
AbstractButton {
id: userNameButton
PowerlevelIndicator {
id: powerlevelIndicator
anchors.left: parent.left
//anchors.horizontalCenter: parent.horizontalCenter
powerlevel: userPowerlevel
height: fontMetrics.lineSpacing
width: fontMetrics.lineSpacing
sourceSize.width: fontMetrics.lineSpacing
sourceSize.height: fontMetrics.lineSpacing
permissions: room ? room.permissions : null
visible: isAdmin || isModerator
}
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: userId
ToolTip.visible: hovered
leftPadding: powerlevelIndicator.visible ? 16 : 0
leftInset: 0
rightInset: 0
rightPadding: 0
contentItem: Label {
id: userName_
color: TimelineManager.userColor(userId, palette.base)
text: TimelineManager.escapeEmoji(userNameTextMetrics.elidedText)
textFormat: Text.RichText
}
onClicked: room.openUserProfile(userId)
TextMetrics {
id: userNameTextMetrics
elide: Text.ElideRight
elideWidth: userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3)
text: userName
}
NhekoCursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
}
Label {
id: statusMsg
property string userStatus: Presence.userStatus(userId)
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("%1's status message").arg(userName)
ToolTip.visible: statusMsgHoverHandler.hovered
anchors.baseline: userNameButton.baseline
color: palette.buttonText
elide: Text.ElideRight
font.italic: true
font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8)
text: userStatus.replace(/\n/g, " ")
textFormat: Text.PlainText
width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing)
HoverHandler {
id: statusMsgHoverHandler
}
Connections {
function onPresenceChanged(id) {
if (id == userId)
statusMsg.userStatus = Presence.userStatus(userId);
}
target: Presence
}
}
}
}

View file

@ -130,21 +130,23 @@ AbstractButton {
id: mxcimage id: mxcimage
visible: loaded visible: loaded
anchors.fill: parent
roomm: room roomm: room
play: !Settings.animateImagesOnHover || parent.hovered play: !Settings.animateImagesOnHover || parent.hovered
eventId: parent.eventId eventId: parent.eventId
width: parent.implicitWidth
height: parent.implicitHeight
} }
Image { Image {
id: blurhash_ id: blurhash_
anchors.fill: parent
source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/image-failed.svg?" + palette.buttonText) source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/image-failed.svg?" + palette.buttonText)
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
sourceSize.width: parent.width * Screen.devicePixelRatio sourceSize.width: parent.implicitWidth * Screen.devicePixelRatio
sourceSize.height: parent.height * Screen.devicePixelRatio sourceSize.height: parent.implicitHeight * Screen.devicePixelRatio
width: parent.implicitWidth
height: parent.implicitHeight
} }
onClicked: Settings.openImageExternal ? room.openMedia(eventId) : TimelineManager.openImageOverlay(room, url, eventId, originalWidth, proportionalHeight); onClicked: Settings.openImageExternal ? room.openMedia(eventId) : TimelineManager.openImageOverlay(room, url, eventId, originalWidth, proportionalHeight);
@ -152,7 +154,8 @@ AbstractButton {
Item { Item {
id: overlay id: overlay
anchors.fill: parent width: parent.implicitWidth
height: parent.implicitHeight
visible: parent.hovered visible: parent.hovered
Rectangle { Rectangle {

View file

@ -44,7 +44,8 @@ MatrixText {
Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight
clip: !keepFullText clip: !keepFullText
selectByMouse: !Settings.mobileMode && !isReply selectByMouse: !Settings.mobileMode && !isReply
enabled: !Settings.mobileMode enabled: !Settings.mobileMode && !isReply
hoverEnabled: !Settings.mobileMode
font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
NhekoCursorShape { NhekoCursorShape {