diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 5ea73fb5..ab8a3ee8 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -68,8 +68,17 @@ Item {
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
}
}
+ Component {
+ id: bubbleMessageStyle
+
+ TimelineBubbleMessageStyle {
+ messageActions: messageActionsC
+ messageContextMenu: messageContextMenuC
+ scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
+ }
+ }
- delegate: defaultMessageStyle
+ delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle
footer: Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Nheko.paddingLarge
diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml
new file mode 100644
index 00000000..c6c1aede
--- /dev/null
+++ b/resources/qml/TimelineBubbleMessageStyle.qml
@@ -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
+ }
+ }
+ ]
+}
+
diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml
index 8beaa8f0..f4906208 100644
--- a/resources/qml/TimelineDefaultMessageStyle.qml
+++ b/resources/qml/TimelineDefaultMessageStyle.qml
@@ -51,7 +51,7 @@ TimelineEvent {
property alias hovered: messageHover.hovered
property bool scrolledToThis: false
- mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4
+ mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0)
replyInset: mainInset + 4 + Nheko.paddingSmall
maxWidth: chat.delegateMaxWidth - avatarMargin - metadata.width
@@ -269,82 +269,24 @@ TimelineEvent {
}
},
- RowLayout {
+ TimelineMetadata {
id: metadata
- property int iconSize: Math.floor(fontMetrics.ascent * scaling)
- property double scaling: Settings.bubbles ? 0.75 : 1
+ scaling: 1
anchors.right: parent.right
y: section.visible && section.active ? section.y + section.height : 0
- spacing: 2
- visible: !isStateEvent
+ visible: !wrapper.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
-
- }
- }
+ eventId: wrapper.eventId
+ status: wrapper.status
+ trustlevel: wrapper.trustlevel
+ isEdited: wrapper.isEdited
+ isEncrypted: wrapper.isEncrypted
+ threadId: wrapper.threadId
+ timestamp: wrapper.timestamp
+ room: wrapper.room
},
Reactions {
id: reactionRow
diff --git a/resources/qml/TimelineMetadata.qml b/resources/qml/TimelineMetadata.qml
new file mode 100644
index 00000000..53282fc5
--- /dev/null
+++ b/resources/qml/TimelineMetadata.qml
@@ -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
+
+ }
+ }
+}
|