diff --git a/src/qml/Component/Timeline/AudioDelegate.qml b/src/qml/Component/Timeline/AudioDelegate.qml index 89dfca9a4..bbbc05e64 100644 --- a/src/qml/Component/Timeline/AudioDelegate.qml +++ b/src/qml/Component/Timeline/AudioDelegate.qml @@ -13,15 +13,13 @@ import org.kde.neochat 1.0 TimelineContainer { id: audioDelegate - onReplyClicked: ListView.view.goToEvent(eventID) - onOpenContextMenu: openFileContext(model, audioDelegate) readonly property bool downloaded: model.progressInfo && model.progressInfo.completed onDownloadedChanged: audio.play() - hoverComponent: hoverActions innerObject: ColumnLayout { + Layout.fillWidth: true Layout.maximumWidth: audioDelegate.contentMaxWidth Audio { diff --git a/src/qml/Component/Timeline/FileDelegate.qml b/src/qml/Component/Timeline/FileDelegate.qml index 76452103c..b62f1022d 100644 --- a/src/qml/Component/Timeline/FileDelegate.qml +++ b/src/qml/Component/Timeline/FileDelegate.qml @@ -13,9 +13,6 @@ import org.kde.neochat 1.0 TimelineContainer { id: fileDelegate - onReplyClicked: ListView.view.goToEvent(eventID) - hoverComponent: hoverActions - onOpenContextMenu: openFileContext(model, fileDelegate) readonly property bool downloaded: progressInfo && progressInfo.completed diff --git a/src/qml/Component/Timeline/ImageDelegate.qml b/src/qml/Component/Timeline/ImageDelegate.qml index 3b3850bcd..ecd186f30 100644 --- a/src/qml/Component/Timeline/ImageDelegate.qml +++ b/src/qml/Component/Timeline/ImageDelegate.qml @@ -13,9 +13,6 @@ import org.kde.neochat 1.0 TimelineContainer { id: imageDelegate - onReplyClicked: ListView.view.goToEvent(eventID) - hoverComponent: hoverActions - onOpenContextMenu: openFileContext(model, imageDelegate) property var content: model.content diff --git a/src/qml/Component/Timeline/MessageDelegate.qml b/src/qml/Component/Timeline/MessageDelegate.qml index 555eb9a99..9e2e7e986 100644 --- a/src/qml/Component/Timeline/MessageDelegate.qml +++ b/src/qml/Component/Timeline/MessageDelegate.qml @@ -16,9 +16,6 @@ TimelineContainer { property bool isEmote: false onOpenContextMenu: openMessageContext(model, label.selectedText, Controller.plainText(label.textDocument)) - onReplyClicked: ListView.view.goToEvent(eventID) - hoverComponent: hoverActions - innerObject: ColumnLayout { Layout.maximumWidth: messageDelegate.contentMaxWidth RichLabel { @@ -29,7 +26,6 @@ TimelineContainer { Loader { id: linkPreviewLoader Layout.fillWidth: true - height: active ? item.implicitHeight : 0 active: !currentRoom.usesEncryption && model.display && model.display.includes("http") visible: Config.showLinkPreview && active sourceComponent: LinkPreviewDelegate { diff --git a/src/qml/Component/Timeline/ReactionDelegate.qml b/src/qml/Component/Timeline/ReactionDelegate.qml index ffcf92b49..92ac37c84 100644 --- a/src/qml/Component/Timeline/ReactionDelegate.qml +++ b/src/qml/Component/Timeline/ReactionDelegate.qml @@ -9,7 +9,8 @@ import QtQuick.Layouts 1.15 import org.kde.kirigami 2.15 as Kirigami Flow { - spacing: Kirigami.Units.largeSpacing + spacing: Kirigami.Units.smallSpacing + Repeater { model: reaction ?? null @@ -32,7 +33,6 @@ Flow { border.width: 1 } - checkable: true checked: modelData.hasLocalUser diff --git a/src/qml/Component/Timeline/TimelineContainer.qml b/src/qml/Component/Timeline/TimelineContainer.qml index 93d013494..799b5e765 100644 --- a/src/qml/Component/Timeline/TimelineContainer.qml +++ b/src/qml/Component/Timeline/TimelineContainer.qml @@ -9,15 +9,21 @@ import org.kde.kirigami 2.15 as Kirigami import org.kde.neochat 1.0 -QQC2.ItemDelegate { - id: timelineContainer +ColumnLayout { + id: root + + signal openContextMenu + signal openExternally() + signal replyClicked(string eventID) + + onReplyClicked: ListView.view.goToEvent(eventID) + default property alias innerObject : column.children - // readonly property bool failed: marks == EventStatus.SendingFailed - - readonly property bool sectionVisible: model.showSection + property Item hoverComponent: hoverActions property bool isEmote: false property bool cardBackground: true + property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && model.author.isLocalUser && !Config.compactLayout property bool isHighlighted: model.isHighlighted || isTemporaryHighlighted property bool isTemporaryHighlighted: false @@ -30,58 +36,15 @@ QQC2.ItemDelegate { onTriggered: isTemporaryHighlighted = false } - signal openContextMenu - // The bubble and delegate widths are allowed to grow once the ListView gets beyond a certain size // extraWidth defines this as the excess after a certain ListView width, capped to a max value readonly property int extraWidth: messageListView.width >= Kirigami.Units.gridUnit * 46 ? Math.min((messageListView.width - Kirigami.Units.gridUnit * 46), Kirigami.Units.gridUnit * 20) : 0 readonly property int bubbleMaxWidth: Kirigami.Units.gridUnit * 20 + extraWidth * 0.5 - readonly property int delegateMaxWidth: Config.compactLayout ? messageListView.width : Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth) + readonly property int delegateWidth: Config.compactLayout ? messageListView.width : Math.min(messageListView.width, Kirigami.Units.gridUnit * 40 + extraWidth) readonly property int contentMaxWidth: Config.compactLayout ? width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) - Kirigami.Units.largeSpacing * 4 : Math.min(width - Kirigami.Units.gridUnit * 2 - Kirigami.Units.largeSpacing * 6, bubbleMaxWidth) - property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && - model.author.isLocalUser && !Config.compactLayout - - signal openExternally() - signal replyClicked(string eventID) - - Component.onCompleted: { - if (model.isReply && model.reply === undefined) { - messageEventModel.loadReply(sortedMessageEventModel.mapToSource(sortedMessageEventModel.index(model.index, 0))) - } - } - - topPadding: 0 - bottomPadding: 0 - topInset: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing) - leftInset: Kirigami.Units.smallSpacing - rightInset: Kirigami.Units.smallSpacing - width: delegateMaxWidth - height: sectionDelegate.height + Math.max(model.showAuthor ? avatar.height : 0, bubble.implicitHeight) + loader.height + loader.anchors.topMargin + avatar.anchors.topMargin - background: Rectangle { - visible: timelineContainer.hovered - color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15) - radius: Kirigami.Units.smallSpacing - } - - property Item hoverComponent - - // show hover actions - onHoveredChanged: { - if (hovered && !Kirigami.Settings.isMobile) { - updateHoverComponent(); - } - } - - // updates the global hover component to point to this delegate, and update its position - function updateHoverComponent() { - if (hoverComponent) { - hoverComponent.delegate = timelineContainer - hoverComponent.bubble = bubble - hoverComponent.updateFunction = updateHoverComponent; - hoverComponent.event = model - } - } + width: delegateWidth + spacing: Kirigami.Units.smallSpacing state: Config.compactLayout ? "alignLeft" : "alignCenter" // Align left when in compact mode and center when using bubbles @@ -89,7 +52,7 @@ QQC2.ItemDelegate { State { name: "alignLeft" AnchorChanges { - target: timelineContainer + target: root anchors.horizontalCenter: undefined anchors.left: parent ? parent.left : undefined } @@ -97,7 +60,7 @@ QQC2.ItemDelegate { State { name: "alignCenter" AnchorChanges { - target: timelineContainer + target: root anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined anchors.left: undefined } @@ -112,216 +75,244 @@ QQC2.ItemDelegate { SectionDelegate { id: sectionDelegate - anchors.left: timelineContainer.left - anchors.right: timelineContainer.right - visible: sectionVisible - height: visible ? implicitHeight : 0 + + Layout.fillWidth: true + visible: model.showSection labelText: model.showSection ? section : "" } - Kirigami.Avatar { - id: avatar - width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 : 0 - height: width - padding: Kirigami.Units.smallSpacing - topInset: Kirigami.Units.smallSpacing - bottomInset: Kirigami.Units.smallSpacing - leftInset: Kirigami.Units.smallSpacing - rightInset: Kirigami.Units.smallSpacing - sourceSize.width: width - sourceSize.height: width - anchors { - top: sectionDelegate.bottom - topMargin: model.showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing) - left: parent.left - leftMargin: Kirigami.Units.smallSpacing - } + QQC2.ItemDelegate { + id: mainContainer - visible: model.showAuthor && - Config.showAvatarInTimeline && - (Config.compactLayout || !showUserMessageOnRight) - name: model.displayNameForInitials - source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : "" - color: model.author.color + Layout.fillWidth: true + Layout.topMargin: showAuthor ? Kirigami.Units.largeSpacing : (Config.compactLayout ? 1 : Kirigami.Units.smallSpacing) + Layout.leftMargin: Kirigami.Units.smallSpacing - MouseArea { - anchors.fill: parent - onClicked: { - userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, { - room: currentRoom, - user: author.object, - displayName: author.displayName, - avatarMediaId: author.avatarMediaId, - avatarUrl: author.avatarUrl - }).open(); + implicitHeight: Math.max(model.showAuthor ? avatar.implicitHeight : 0, bubble.height) + + Component.onCompleted: { + if (model.isReply && model.reply === undefined) { + messageEventModel.loadReply(sortedMessageEventModel.mapToSource(sortedMessageEventModel.index(model.index, 0))) } - cursorShape: Qt.PointingHandCursor } - } - QQC2.Control { - id: bubble - topPadding: Config.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing - bottomPadding: Config.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing - leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing - rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing - hoverEnabled: true - - anchors { - top: avatar.top - leftMargin: Kirigami.Units.smallSpacing - rightMargin: showUserMessageOnRight ? Kirigami.Units.smallSpacing : Kirigami.Units.largeSpacing + // show hover actions + onHoveredChanged: { + if (hovered && !Kirigami.Settings.isMobile) { + updateHoverComponent(); + } } - // HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably - width: Config.compactLayout ? timelineContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth - state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft" - // states for anchor animations on window resize - // as setting anchors to undefined did not work reliably - states: [ - State { - name: "userMessageOnRight" - AnchorChanges { - target: bubble - anchors.left: undefined - anchors.right: parent.right + + // Show hover actions by updating the global hover component to this delegate + function updateHoverComponent() { + if (hovered && !Kirigami.Settings.isMobile) { + hoverComponent.delegate = root + hoverComponent.bubble = bubble + hoverComponent.event = model + hoverComponent.updateFunction = updateHoverComponent; + } + } + + Kirigami.Avatar { + id: avatar + width: visible || Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 : 0 + height: width + padding: Kirigami.Units.smallSpacing + topInset: Kirigami.Units.smallSpacing + bottomInset: Kirigami.Units.smallSpacing + leftInset: Kirigami.Units.smallSpacing + rightInset: Kirigami.Units.smallSpacing + anchors { + left: parent.left + leftMargin: Kirigami.Units.smallSpacing + } + + visible: model.showAuthor && + Config.showAvatarInTimeline && + (Config.compactLayout || !showUserMessageOnRight) + name: model.author.name ?? model.author.displayName + source: visible && model.author.avatarMediaId ? ("image://mxc/" + model.author.avatarMediaId) : "" + color: model.author.color + + MouseArea { + anchors.fill: parent + onClicked: { + userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, { + room: currentRoom, + user: author.object, + displayName: author.displayName, + avatarMediaId: author.avatarMediaId, + avatarUrl: author.avatarUrl + }).open(); } - }, - State { - name: "userMessageOnLeft" - AnchorChanges { - target: bubble - anchors.left: avatar.right - anchors.right: undefined + cursorShape: Qt.PointingHandCursor + } + } + + QQC2.Control { + id: bubble + topPadding: Config.compactLayout ? Kirigami.Units.smallSpacing / 2 : Kirigami.Units.largeSpacing + bottomPadding: Config.compactLayout ? Kirigami.Units.mediumSpacing / 2 : Kirigami.Units.largeSpacing + leftPadding: Config.compactLayout ? 0 : Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing + rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing + hoverEnabled: true + + anchors { + leftMargin: Kirigami.Units.smallSpacing + rightMargin: Kirigami.Units.largeSpacing + } + // HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably + width: Config.compactLayout ? mainContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth + + state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft" + // states for anchor animations on window resize + // as setting anchors to undefined did not work reliably + states: [ + State { + name: "userMessageOnRight" + AnchorChanges { + target: bubble + anchors.left: undefined + anchors.right: parent.right + } + }, + State { + name: "userMessageOnLeft" + AnchorChanges { + target: bubble + anchors.left: avatar.right + anchors.right: undefined + } } - } - ] + ] - transitions: [ - Transition { - AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic} - } - ] - - contentItem: ColumnLayout { - id: column - spacing: Kirigami.Units.smallSpacing - RowLayout { - id: rowLayout + transitions: [ + Transition { + AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic} + } + ] + contentItem: ColumnLayout { + id: column spacing: Kirigami.Units.smallSpacing - visible: model.showAuthor && !isEmote + RowLayout { + id: rowLayout - QQC2.Label { - id: nameLabel + spacing: Kirigami.Units.smallSpacing + visible: model.showAuthor && !isEmote - Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing + QQC2.Label { + id: nameLabel - text: visible ? author.displayName : "" - textFormat: Text.PlainText - font.weight: Font.Bold - color: author.color - elide: Text.ElideRight - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, { - room: currentRoom, - user: author.object, - displayName: author.displayName, - avatarMediaId: author.avatarMediaId, - avatarUrl: author.avatarUrl - }).open(); + Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing + + text: visible ? author.displayName : "" + textFormat: Text.PlainText + font.weight: Font.Bold + color: author.color + elide: Text.ElideRight + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + userDetailDialog.createObject(QQC2.ApplicationWindow.overlay, { + room: currentRoom, + user: author.object, + displayName: author.displayName, + avatarMediaId: author.avatarMediaId, + avatarUrl: author.avatarUrl + }).open(); + } + } + } + QQC2.Label { + id: timeLabel + + text: visible ? time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : "" + color: Kirigami.Theme.disabledTextColor + QQC2.ToolTip.visible: hoverHandler.hovered + QQC2.ToolTip.text: time.toLocaleString(Qt.locale(), Locale.LongFormat) + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + + HoverHandler { + id: hoverHandler } } } - QQC2.Label { - id: timeLabel + Loader { + id: replyLoader - text: visible ? time.toLocaleTimeString(Qt.locale(), Locale.ShortFormat) : "" - color: Kirigami.Theme.disabledTextColor - QQC2.ToolTip.visible: hoverHandler.hovered - QQC2.ToolTip.text: time.toLocaleString(Qt.locale(), Locale.LongFormat) - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + Layout.maximumWidth: contentMaxWidth - HoverHandler { - id: hoverHandler + active: model.reply !== undefined + visible: active + + sourceComponent: ReplyComponent { + name: currentRoom.htmlSafeMemberName(reply.author.id) + avatar: reply.author.avatarMediaId ? ("image://mxc/" + reply.author.avatarMediaId) : "" + color: reply.author.color + } + + Connections { + target: replyLoader.item + function onReplyClicked() { + replyClicked(reply.eventId) + } } } } - Loader { - id: replyLoader - Layout.maximumWidth: contentMaxWidth + background: Item { + Kirigami.ShadowedRectangle { + id: bubbleBackground + visible: cardBackground && !Config.compactLayout + anchors.fill: parent + color: { + if (model.author.isLocalUser) { + return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15) + } else if (root.isHighlighted) { + return Kirigami.Theme.positiveBackgroundColor + } else { + return Kirigami.Theme.backgroundColor + } + } + radius: Kirigami.Units.smallSpacing + shadow.size: Kirigami.Units.smallSpacing + shadow.color: root.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10) + border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15) + border.width: 1 - active: model.reply !== undefined - visible: active - - sourceComponent: ReplyComponent { - name: currentRoom.htmlSafeMemberName(reply.author.id) - avatar: reply.author.avatarMediaId ? ("image://mxc/" + reply.author.avatarMediaId) : "" - color: reply.author.color - } - - Connections { - target: replyLoader.item - function onReplyClicked() { - replyClicked(reply.eventId) + Behavior on color { + ColorAnimation {target: bubbleBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic} } } } } - background: Item { - Kirigami.ShadowedRectangle { - id: bubbleBackground - visible: cardBackground && !Config.compactLayout - anchors.fill: parent - color: { - if (model.author.isLocalUser) { - return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15) - } else if (timelineContainer.isHighlighted) { - return Kirigami.Theme.positiveBackgroundColor - } else { - return Kirigami.Theme.backgroundColor - } - } - radius: Kirigami.Units.smallSpacing - shadow.size: Kirigami.Units.smallSpacing - shadow.color: timelineContainer.isHighlighted ? Qt.rgba(0.0, 0.0, 0.0, 0.10) : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.10) - border.color: Kirigami.ColorUtils.tintWithAlpha(color, Kirigami.Theme.textColor, 0.15) - border.width: 1 + background: Rectangle { + visible: mainContainer.hovered + color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15) + radius: Kirigami.Units.smallSpacing + } - Behavior on color { - ColorAnimation {target: bubbleBackground; duration: Kirigami.Units.veryLongDuration; easing.type: Easing.InOutCubic} - } - } + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: root.openContextMenu() + } + + TapHandler { + acceptedButtons: Qt.LeftButton + onLongPressed: root.openContextMenu() } } - Loader { - id: loader - anchors { - left: bubble.left - right: parent.right - top: bubble.bottom - topMargin: active ? Kirigami.Units.smallSpacing : 0 - } - height: active ? item.implicitHeight : 0 - active: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0 - visible: active - sourceComponent: ReactionDelegate { } - } + ReactionDelegate { + Layout.maximumWidth: delegateWidth - Kirigami.Units.largeSpacing * 2 + Layout.alignment: showUserMessageOnRight ? Qt.AlignRight : Qt.AlignLeft + Layout.leftMargin: showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin + Layout.rightMargin: showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0 - TapHandler { - acceptedButtons: Qt.RightButton - acceptedDevices: PointerDevice.Mouse - onTapped: timelineContainer.openContextMenu() - } - - TapHandler { - acceptedButtons: Qt.LeftButton - onLongPressed: timelineContainer.openContextMenu() + visible: eventType !== MessageEventModel.State && eventType !== MessageEventModel.Notice && reaction != undefined && reaction.length > 0 } } diff --git a/src/qml/Component/Timeline/VideoDelegate.qml b/src/qml/Component/Timeline/VideoDelegate.qml index 2e14965e3..855398d8c 100644 --- a/src/qml/Component/Timeline/VideoDelegate.qml +++ b/src/qml/Component/Timeline/VideoDelegate.qml @@ -14,9 +14,6 @@ import org.kde.neochat 1.0 TimelineContainer { id: videoDelegate - onReplyClicked: ListView.view.goToEvent(eventID) - hoverComponent: hoverActions - property bool playOnFinished: false readonly property bool downloaded: progressInfo && progressInfo.completed diff --git a/src/qml/Page/RoomPage.qml b/src/qml/Page/RoomPage.qml index edaa83f13..979da3d42 100644 --- a/src/qml/Page/RoomPage.qml +++ b/src/qml/Page/RoomPage.qml @@ -463,6 +463,7 @@ Kirigami.ScrollablePage { property var bubble: null property var hovered: bubble && bubble.hovered property var visibleDelayed: (hovered || hoverHandler.hovered) && !Kirigami.Settings.isMobile + property var updateFunction onVisibleDelayedChanged: if (visibleDelayed) { visible = true; } else { @@ -482,8 +483,6 @@ Kirigami.ScrollablePage { visible: false - property var updateFunction - property alias childWidth: hoverActionsRow.width property alias childHeight: hoverActionsRow.height