diff options
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | resources/qml/MessageView.qml | 11 | ||||
-rw-r--r-- | resources/qml/TimelineBubbleMessageStyle.qml | 323 | ||||
-rw-r--r-- | resources/qml/TimelineDefaultMessageStyle.qml | 82 | ||||
-rw-r--r-- | resources/qml/TimelineMetadata.qml | 98 |
5 files changed, 445 insertions, 71 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index cbe2b20e..a426f5d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -703,6 +703,8 @@ set(QML_SOURCES resources/qml/RoomList.qml resources/qml/TimelineSectionHeader.qml resources/qml/TimelineDefaultMessageStyle.qml + resources/qml/TimelineBubbleMessageStyle.qml + resources/qml/TimelineMetadata.qml resources/qml/TimelineView.qml resources/qml/Avatar.qml resources/qml/Completer.qml 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 + + } + } +} |