From 6d56251f6fb958943855d896d3f51ef8f4b87327 Mon Sep 17 00:00:00 2001 From: James Graham Date: Fri, 22 Sep 2023 17:12:56 +0000 Subject: [PATCH] Fix the timeline Part 2: Bubble Rework This reworks the bubble as a separate component and makes some fixes to prevent the console being spammed with polish loop warnings. --- src/qml/Component/Timeline/AudioDelegate.qml | 5 +- src/qml/Component/Timeline/Bubble.qml | 226 +++++++++ .../Component/Timeline/EncryptedDelegate.qml | 3 +- src/qml/Component/Timeline/FileDelegate.qml | 4 +- src/qml/Component/Timeline/ImageDelegate.qml | 8 +- .../Timeline/LiveLocationDelegate.qml | 4 +- .../Component/Timeline/LocationDelegate.qml | 4 +- .../Component/Timeline/MessageDelegate.qml | 440 +++++++----------- src/qml/Component/Timeline/PollDelegate.qml | 3 +- src/qml/Component/Timeline/ReplyComponent.qml | 8 +- src/qml/Component/Timeline/TextDelegate.qml | 3 +- src/qml/Component/Timeline/VideoDelegate.qml | 6 +- src/qml/Component/TimelineView.qml | 3 - src/res.qrc | 1 + 14 files changed, 412 insertions(+), 306 deletions(-) create mode 100644 src/qml/Component/Timeline/Bubble.qml diff --git a/src/qml/Component/Timeline/AudioDelegate.qml b/src/qml/Component/Timeline/AudioDelegate.qml index 5907ae488..8764c08c0 100644 --- a/src/qml/Component/Timeline/AudioDelegate.qml +++ b/src/qml/Component/Timeline/AudioDelegate.qml @@ -38,10 +38,7 @@ MessageDelegate { onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo) - innerObject: ColumnLayout { - Layout.fillWidth: true - Layout.maximumWidth: root.contentMaxWidth - + bubbleContent: ColumnLayout { MediaPlayer { id: audio source: root.progressInfo.localPath diff --git a/src/qml/Component/Timeline/Bubble.qml b/src/qml/Component/Timeline/Bubble.qml new file mode 100644 index 000000000..56cbc5581 --- /dev/null +++ b/src/qml/Component/Timeline/Bubble.qml @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: 2023 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +import QtQuick 2.15 +import QtQuick.Controls 2.15 as QQC2 +import QtQuick.Layouts 1.15 + +import org.kde.kirigami 2.15 as Kirigami + +import org.kde.neochat 1.0 + +/** + * @brief A chat bubble for displaying the content of message events. + * + * The content of the bubble is set via the content property which is then managed + * by the bubble to apply the correct sizing (including limiting the width if a + * maxContentWidth is set). + * + * The bubble also supports a header with the author and message timestamp and a + * reply. + */ +QQC2.Control { + id: root + + /** + * @brief The message author. + * + * This should consist of the following: + * - id - The matrix ID of the author. + * - isLocalUser - Whether the author is the local user. + * - avatarSource - The mxc URL for the author's avatar in the current room. + * - avatarMediaId - The media ID of the author's avatar. + * - avatarUrl - The mxc URL for the author's avatar. + * - displayName - The display name of the author. + * - display - The name of the author. + * - color - The color for the author. + * - object - The Quotient::User object for the author. + * + * @sa Quotient::User + */ + property var author + + /** + * @brief The timestamp of the message. + */ + property var time + + /** + * @brief The timestamp of the message as a string. + */ + property string timeString + + /** + * @brief Whether the message should be highlighted. + */ + property bool showHighlight: false + + /** + * @brief The main delegate content item to show in the bubble. + */ + property Item content + + /** + * @brief Whether this message is replying to another. + */ + property bool isReply: false + + /** + * @brief The matrix ID of the reply event. + */ + required property var replyId + + /** + * @brief The reply author. + * + * This should consist of the following: + * - id - The matrix ID of the reply author. + * - isLocalUser - Whether the reply author is the local user. + * - avatarSource - The mxc URL for the reply author's avatar in the current room. + * - avatarMediaId - The media ID of the reply author's avatar. + * - avatarUrl - The mxc URL for the reply author's avatar. + * - displayName - The display name of the reply author. + * - display - The name of the reply author. + * - color - The color for the reply author. + * - object - The Quotient::User object for the reply author. + * + * @sa Quotient::User + */ + required property var replyAuthor + + /** + * @brief The delegate type of the message replied to. + */ + required property int replyDelegateType + + /** + * @brief The display text of the message replied to. + */ + required property string replyDisplay + + /** + * @brief The media info for the reply event. + * + * This could be an image, audio, video or file. + * + * This should consist of the following: + * - source - The mxc URL for the media. + * - mimeType - The MIME type of the media. + * - mimeIcon - The MIME icon name. + * - size - The file size in bytes. + * - duration - The length in seconds of the audio media (audio/video only). + * - width - The width in pixels of the audio media (image/video only). + * - height - The height in pixels of the audio media (image/video only). + * - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only). + */ + required property var replyMediaInfo + + /** + * @brief Whether the bubble background should be shown. + */ + property alias showBackground: bubbleBackground.visible + + /** + * @brief The maximum width that the bubble's content can be. + */ + property real maxContentWidth: -1 + + /** + * @brief The reply has been clicked. + */ + signal replyClicked(string eventID) + + contentItem: ColumnLayout { + RowLayout { + Layout.maximumWidth: root.maxContentWidth + QQC2.Label { + Layout.fillWidth: true + text: root.author.displayName + color: root.author.color + textFormat: Text.PlainText + font.weight: Font.Bold + elide: Text.ElideRight + + TapHandler { + onTapped: RoomManager.visitUser(root.author.object, "mention") + } + HoverHandler { + cursorShape: Qt.PointingHandCursor + } + } + QQC2.Label { + text: root.timeString + horizontalAlignment: Text.AlignRight + color: Kirigami.Theme.disabledTextColor + QQC2.ToolTip.visible: timeHoverHandler.hovered + QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.LongFormat) + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + + HoverHandler { + id: timeHoverHandler + } + } + } + Loader { + id: replyLoader + Layout.fillWidth: true + Layout.maximumWidth: root.maxContentWidth + + active: root.isReply && root.replyDelegateType !== DelegateType.Other + visible: active + + sourceComponent: ReplyComponent { + author: root.replyAuthor + type: root.replyDelegateType + display: root.replyDisplay + mediaInfo: root.replyMediaInfo + contentMaxWidth: root.maxContentWidth + } + + Connections { + target: replyLoader.item + function onReplyClicked() { + replyClicked(root.replyId) + } + } + } + Item { + id: contentParent + Layout.fillWidth: true + Layout.maximumWidth: root.maxContentWidth + implicitWidth: root.content ? root.content.implicitWidth : 0 + implicitHeight: root.content ? root.content.implicitHeight : 0 + } + } + + background: Kirigami.ShadowedRectangle { + id: bubbleBackground + visible: root.showBackground + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + color: if (root.author.isLocalUser) { + return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15) + } else if (root.showHighlight) { + return Kirigami.Theme.positiveBackgroundColor + } else { + return Kirigami.Theme.backgroundColor + } + radius: Kirigami.Units.smallSpacing + shadow { + size: Kirigami.Units.smallSpacing + color: root.showHighlight ? 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) + } + + Behavior on color { + ColorAnimation { duration: Kirigami.Units.shortDuration } + } + } + + onContentChanged: { + if (!root.content) { + return; + } + root.content.parent = contentParent; + root.content.anchors.fill = contentParent; + } +} diff --git a/src/qml/Component/Timeline/EncryptedDelegate.qml b/src/qml/Component/Timeline/EncryptedDelegate.qml index 77bd37d8a..d14dc0aa8 100644 --- a/src/qml/Component/Timeline/EncryptedDelegate.qml +++ b/src/qml/Component/Timeline/EncryptedDelegate.qml @@ -15,7 +15,7 @@ import org.kde.neochat MessageDelegate { id: encryptedDelegate - innerObject: TextEdit { + bubbleContent: TextEdit { text: i18n("This message is encrypted and the sender has not shared the key with this device.") color: Kirigami.Theme.disabledTextColor selectedTextColor: Kirigami.Theme.highlightedTextColor @@ -25,7 +25,6 @@ MessageDelegate { readOnly: true wrapMode: Text.WordWrap textFormat: Text.RichText - Layout.maximumWidth: encryptedDelegate.contentMaxWidth Layout.leftMargin: Config.showAvatarInTimeline ? Kirigami.Units.largeSpacing : 0 } } diff --git a/src/qml/Component/Timeline/FileDelegate.qml b/src/qml/Component/Timeline/FileDelegate.qml index 34fe005e1..9b7483e86 100644 --- a/src/qml/Component/Timeline/FileDelegate.qml +++ b/src/qml/Component/Timeline/FileDelegate.qml @@ -55,9 +55,7 @@ MessageDelegate { UrlHelper.openUrl(root.progressInfo.localPath); } - innerObject: RowLayout { - Layout.maximumWidth: Math.min(root.contentMaxWidth, implicitWidth) - + bubbleContent: RowLayout { spacing: Kirigami.Units.largeSpacing states: [ diff --git a/src/qml/Component/Timeline/ImageDelegate.qml b/src/qml/Component/Timeline/ImageDelegate.qml index 531dbd0bc..decf8819d 100644 --- a/src/qml/Component/Timeline/ImageDelegate.qml +++ b/src/qml/Component/Timeline/ImageDelegate.qml @@ -55,15 +55,13 @@ MessageDelegate { onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, "", "", mediaInfo.mimeType, progressInfo) - innerObject: Item { + bubbleContent: Item { id: imageContainer - Layout.preferredWidth: mediaSizeHelper.currentSize.width - Layout.preferredHeight: mediaSizeHelper.currentSize.height property var imageItem: root.mediaInfo.animated ? animatedImageLoader.item : imageLoader.item - implicitWidth: root.mediaInfo.animated ? animatedImageLoader.width : imageLoader.width - implicitHeight: root.mediaInfo.animated ? animatedImageLoader.height : imageLoader.height + implicitWidth: mediaSizeHelper.currentSize.width + implicitHeight: mediaSizeHelper.currentSize.height Loader { id: imageLoader diff --git a/src/qml/Component/Timeline/LiveLocationDelegate.qml b/src/qml/Component/Timeline/LiveLocationDelegate.qml index 3f2a3c671..2b84e3dfe 100644 --- a/src/qml/Component/Timeline/LiveLocationDelegate.qml +++ b/src/qml/Component/Timeline/LiveLocationDelegate.qml @@ -20,9 +20,7 @@ MessageDelegate { property alias room: liveLocationModel.room - ColumnLayout { - Layout.maximumWidth: root.contentMaxWidth - Layout.preferredWidth: root.contentMaxWidth + bubbleContent: ColumnLayout { LiveLocationsModel { id: liveLocationModel eventId: root.eventId diff --git a/src/qml/Component/Timeline/LocationDelegate.qml b/src/qml/Component/Timeline/LocationDelegate.qml index f8ea6faba..6816d1a2c 100644 --- a/src/qml/Component/Timeline/LocationDelegate.qml +++ b/src/qml/Component/Timeline/LocationDelegate.qml @@ -35,9 +35,7 @@ MessageDelegate { */ required property string asset - ColumnLayout { - Layout.maximumWidth: root.contentMaxWidth - Layout.preferredWidth: root.contentMaxWidth + bubbleContent: ColumnLayout { Map { id: map Layout.fillWidth: true diff --git a/src/qml/Component/Timeline/MessageDelegate.qml b/src/qml/Component/Timeline/MessageDelegate.qml index 9a2a7a32f..2056da85d 100644 --- a/src/qml/Component/Timeline/MessageDelegate.qml +++ b/src/qml/Component/Timeline/MessageDelegate.qml @@ -29,271 +29,260 @@ TimelineDelegate { id: root /** - * @brief The index of the delegate in the model. - */ + * @brief The index of the delegate in the model. + */ required property var index /** - * @brief The matrix ID of the message event. - */ + * @brief The matrix ID of the message event. + */ required property string eventId /** - * @brief The timestamp of the message. - */ + * @brief The timestamp of the message. + */ required property var time /** - * @brief The timestamp of the message as a string. - */ + * @brief The timestamp of the message as a string. + */ required property string timeString /** - * @brief The message author. - * - * This should consist of the following: - * - id - The matrix ID of the author. - * - isLocalUser - Whether the author is the local user. - * - avatarSource - The mxc URL for the author's avatar in the current room. - * - avatarMediaId - The media ID of the author's avatar. - * - avatarUrl - The mxc URL for the author's avatar. - * - displayName - The display name of the author. - * - display - The name of the author. - * - color - The color for the author. - * - object - The Quotient::User object for the author. - * - * @sa Quotient::User - */ + * @brief The message author. + * + * This should consist of the following: + * - id - The matrix ID of the author. + * - isLocalUser - Whether the author is the local user. + * - avatarSource - The mxc URL for the author's avatar in the current room. + * - avatarMediaId - The media ID of the author's avatar. + * - avatarUrl - The mxc URL for the author's avatar. + * - displayName - The display name of the author. + * - display - The name of the author. + * - color - The color for the author. + * - object - The Quotient::User object for the author. + * + * @sa Quotient::User + */ required property var author /** - * @brief Whether the author should be shown. - */ + * @brief Whether the author should be shown. + */ required property bool showAuthor /** - * @brief Whether the author should always be shown. - * - * This is primarily used when these delegates are used in a filtered list of - * events rather than a sequential timeline, e.g. the media model view. - * - * @note This setting still respects the avatar configuration settings. - */ + * @brief Whether the author should always be shown. + * + * This is primarily used when these delegates are used in a filtered list of + * events rather than a sequential timeline, e.g. the media model view. + * + * @note This setting still respects the avatar configuration settings. + */ property bool alwaysShowAuthor: false /** - * @brief The delegate type of the message. - */ + * @brief The delegate type of the message. + */ required property int delegateType /** - * @brief The display text of the message. - */ + * @brief The display text of the message. + */ required property string display /** - * @brief The display text of the message as plain text. - */ + * @brief The display text of the message as plain text. + */ required property string plainText /** - * @brief The date of the event as a string. - */ + * @brief The date of the event as a string. + */ required property string section /** - * @brief Whether the section header should be shown. - */ + * @brief Whether the section header should be shown. + */ required property bool showSection /** - * @brief A model with the reactions to the message in. - */ + * @brief A model with the reactions to the message in. + */ required property var reaction /** - * @brief Whether the reaction component should be shown. - */ + * @brief Whether the reaction component should be shown. + */ required property bool showReactions /** - * @brief A model with the first 5 other user read markers for this message. - */ + * @brief A model with the first 5 other user read markers for this message. + */ required property var readMarkers /** - * @brief String with the display name and matrix ID of the other user read markers. - */ + * @brief String with the display name and matrix ID of the other user read markers. + */ required property string readMarkersString /** - * @brief The number of other users at the event after the first 5. - */ + * @brief The number of other users at the event after the first 5. + */ required property var excessReadMarkers /** - * @brief Whether the other user read marker component should be shown. - */ + * @brief Whether the other user read marker component should be shown. + */ required property bool showReadMarkers /** - * @brief The matrix ID of the reply event. - */ + * @brief The matrix ID of the reply event. + */ required property var replyId /** - * @brief The reply author. - * - * This should consist of the following: - * - id - The matrix ID of the reply author. - * - isLocalUser - Whether the reply author is the local user. - * - avatarSource - The mxc URL for the reply author's avatar in the current room. - * - avatarMediaId - The media ID of the reply author's avatar. - * - avatarUrl - The mxc URL for the reply author's avatar. - * - displayName - The display name of the reply author. - * - display - The name of the reply author. - * - color - The color for the reply author. - * - object - The Quotient::User object for the reply author. - * - * @sa Quotient::User - */ + * @brief The reply author. + * + * This should consist of the following: + * - id - The matrix ID of the reply author. + * - isLocalUser - Whether the reply author is the local user. + * - avatarSource - The mxc URL for the reply author's avatar in the current room. + * - avatarMediaId - The media ID of the reply author's avatar. + * - avatarUrl - The mxc URL for the reply author's avatar. + * - displayName - The display name of the reply author. + * - display - The name of the reply author. + * - color - The color for the reply author. + * - object - The Quotient::User object for the reply author. + * + * @sa Quotient::User + */ required property var replyAuthor /** - * @brief The delegate type of the message replied to. - */ + * @brief The delegate type of the message replied to. + */ required property int replyDelegateType /** - * @brief The display text of the message replied to. - */ + * @brief The display text of the message replied to. + */ required property string replyDisplay /** - * @brief The media info for the reply event. - * - * This could be an image, audio, video or file. - * - * This should consist of the following: - * - source - The mxc URL for the media. - * - mimeType - The MIME type of the media. - * - mimeIcon - The MIME icon name. - * - size - The file size in bytes. - * - duration - The length in seconds of the audio media (audio/video only). - * - width - The width in pixels of the audio media (image/video only). - * - height - The height in pixels of the audio media (image/video only). - * - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only). - */ + * @brief The media info for the reply event. + * + * This could be an image, audio, video or file. + * + * This should consist of the following: + * - source - The mxc URL for the media. + * - mimeType - The MIME type of the media. + * - mimeIcon - The MIME icon name. + * - size - The file size in bytes. + * - duration - The length in seconds of the audio media (audio/video only). + * - width - The width in pixels of the audio media (image/video only). + * - height - The height in pixels of the audio media (image/video only). + * - tempInfo - mediaInfo (with the same properties as this except no tempInfo) for a temporary image while the file downloads (image/video only). + */ required property var replyMediaInfo /** - * @brief Whether this message is replying to another. - */ + * @brief Whether this message is replying to another. + */ required property bool isReply /** - * @brief Whether this message has a local user mention. - */ + * @brief Whether this message has a local user mention. + */ required property bool isHighlighted /** - * @brief Whether an event is waiting to be accepted by the server. - */ + * @brief Whether an event is waiting to be accepted by the server. + */ required property bool isPending /** - * @brief Progress info when downloading files. - * - * @sa Quotient::FileTransferInfo - */ + * @brief Progress info when downloading files. + * + * @sa Quotient::FileTransferInfo + */ required property var progressInfo /** - * @brief Whether an encrypted message is sent in a verified session. - */ + * @brief Whether an encrypted message is sent in a verified session. + */ required property bool verified /** - * @brief The x position of the message bubble. - * - * @note Used for positioning the hover actions. - */ + * @brief The x position of the message bubble. + * + * @note Used for positioning the hover actions. + */ readonly property real bubbleX: bubble.x + bubble.anchors.leftMargin /** - * @brief The y position of the message bubble. - * - * @note Used for positioning the hover actions. - */ + * @brief The y position of the message bubble. + * + * @note Used for positioning the hover actions. + */ readonly property alias bubbleY: mainContainer.y /** - * @brief The width of the message bubble. - * - * @note Used for sizing the hover actions. - */ + * @brief The width of the message bubble. + * + * @note Used for sizing the hover actions. + */ readonly property alias bubbleWidth: bubble.width /** - * @brief Whether this message is hovered. - */ + * @brief Whether this message is hovered. + */ readonly property alias hovered: bubble.hovered required property NeoChatConnection connection /** - * @brief Open the context menu for the message. - */ + * @brief Open the context menu for the message. + */ signal openContextMenu /** - * @brief Open the any message media externally. - */ + * @brief Open the any message media externally. + */ signal openExternally() /** - * @brief The reply has been clicked. - */ + * @brief The reply has been clicked. + */ signal replyClicked(string eventID) - onReplyClicked: eventID => ListView.view.goToEvent(eventID) /** - * @brief The component to display the delegate type. - * - * This is used by the inherited delegates to assign a component to visualise - * the message content for that delegate type. - */ - default property alias innerObject : column.children + * @brief The main delegate content item to show in the bubble. + */ + property alias bubbleContent: bubble.content /** - * @brief Whether the bubble background is enabled. - */ + * @brief Whether the bubble background is enabled. + */ property bool cardBackground: true /** - * @brief Whether the delegate should always stretch to the maximum availabel width. - */ + * @brief Whether the delegate should always stretch to the maximum availabel width. + */ property bool alwaysMaxWidth: false /** - * @brief Whether local user messages should be aligned right. - * - * TODO: make private - */ - property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !alwaysMaxWidth - - /** - * @brief Whether the message should be highlighted. - */ + * @brief Whether the message should be highlighted. + */ property bool showHighlight: root.isHighlighted || isTemporaryHighlighted /** - * @brief Whether the message should temporarily be highlighted. - * - * Normally triggered when jumping to the event in the timeline, e.g. when a reply - * is clicked. - */ + * @brief Whether the message should temporarily be highlighted. + * + * Normally triggered when jumping to the event in the timeline, e.g. when a reply + * is clicked. + */ property bool isTemporaryHighlighted: false onIsTemporaryHighlightedChanged: if (isTemporaryHighlighted) temporaryHighlightTimer.start() @@ -308,7 +297,7 @@ TimelineDelegate { /** * @brief The width available to the bubble content. */ - property alias contentMaxWidth: bubbleSizeHelper.currentWidth + property real contentMaxWidth: bubbleSizeHelper.currentWidth - bubble.leftPadding - bubble.rightPadding contentItem: ColumnLayout { spacing: Kirigami.Units.smallSpacing @@ -356,7 +345,7 @@ TimelineDelegate { visible: (root.showAuthor || root.alwaysShowAuthor) && Config.showAvatarInTimeline && - (Config.compactLayout || !showUserMessageOnRight) + (Config.compactLayout || !_private.showUserMessageOnRight) name: root.author.displayName source: root.author.avatarSource color: root.author.color @@ -369,24 +358,19 @@ TimelineDelegate { cursorShape: Qt.PointingHandCursor } } - - QQC2.Control { + Bubble { id: bubble + anchors.left: avatar.right + anchors.leftMargin: Kirigami.Units.largeSpacing + anchors.rightMargin: Kirigami.Units.largeSpacing + maxContentWidth: root.contentMaxWidth + 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 { - left: avatar.right - leftMargin: Kirigami.Units.largeSpacing - rightMargin: Kirigami.Units.largeSpacing - } - // HACK: anchoring didn't reset anchors.right when switching from parent.right to undefined reliably - width: Config.compactLayout || root.alwaysMaxWidth ? mainContainer.width - (Config.showAvatarInTimeline ? Kirigami.Units.gridUnit * 2 : 0) + Kirigami.Units.largeSpacing * 2 : implicitWidth - - state: showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft" + state: _private.showUserMessageOnRight ? "userMessageOnRight" : "userMessageOnLeft" // states for anchor animations on window resize // as setting anchors to undefined did not work reliably states: [ @@ -408,119 +392,22 @@ TimelineDelegate { } ] - transitions: [ - Transition { - AnchorAnimation{duration: Kirigami.Units.longDuration; easing.type: Easing.OutCubic} - } - ] + author: root.author + time: root.time + timeString: root.timeString - contentItem: RowLayout { - Kirigami.Icon { - source: "content-loading-symbolic" - width: height - Layout.preferredWidth: Kirigami.Units.iconSizes.small - Layout.preferredHeight: Kirigami.Units.iconSizes.small - visible: root.isPending && Config.showLocalMessagesOnRight - } - ColumnLayout { - id: column - spacing: Kirigami.Units.smallSpacing - RowLayout { - id: rowLayout + showHighlight: root.showHighlight - spacing: Kirigami.Units.smallSpacing - visible: root.showAuthor || root.alwaysShowAuthor + isReply: root.isReply + replyId: root.replyId + replyAuthor: root.replyAuthor + replyDelegateType: root.replyDelegateType + replyDisplay: root.replyDisplay + replyMediaInfo: root.replyMediaInfo - QQC2.Label { - id: nameLabel + onReplyClicked: (eventId) => {root.replyClicked(eventId)} - Layout.maximumWidth: contentMaxWidth - timeLabel.implicitWidth - rowLayout.spacing - - text: visible ? root.author.displayName : "" - textFormat: Text.PlainText - font.weight: Font.Bold - color: root.author.color - elide: Text.ElideRight - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: { - RoomManager.visitUser(root.author.object, "mention") - } - } - } - QQC2.Label { - id: timeLabel - - text: root.timeString - color: Kirigami.Theme.disabledTextColor - QQC2.ToolTip.visible: hoverHandler.hovered - QQC2.ToolTip.text: root.time.toLocaleString(Qt.locale(), Locale.LongFormat) - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - - HoverHandler { - id: hoverHandler - } - } - } - Loader { - id: replyLoader - - Layout.maximumWidth: contentMaxWidth - - active: root.isReply && root.replyDelegateType !== DelegateType.Other - visible: active - - sourceComponent: ReplyComponent { - author: root.replyAuthor - type: root.replyDelegateType - display: root.replyDisplay - mediaInfo: root.replyMediaInfo - contentMaxWidth: bubbleSizeHelper.currentWidth - } - - Connections { - target: replyLoader.item - function onReplyClicked() { - replyClicked(root.replyId) - } - } - } - } - Kirigami.Icon { - source: "content-loading-symbolic" - width: height - Layout.preferredWidth: Kirigami.Units.iconSizes.small - Layout.preferredHeight: Kirigami.Units.iconSizes.small - visible: root.isPending && !Config.showLocalMessagesOnRight - } - } - - background: Item { - Kirigami.ShadowedRectangle { - id: bubbleBackground - visible: cardBackground && !Config.compactLayout - anchors.fill: parent - Kirigami.Theme.colorSet: Kirigami.Theme.View - Kirigami.Theme.inherit: false - color: if (root.author.isLocalUser) { - return Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.highlightColor, 0.15) - } else if (root.showHighlight) { - return Kirigami.Theme.positiveBackgroundColor - } else { - return Kirigami.Theme.backgroundColor - } - radius: Kirigami.Units.smallSpacing - shadow { - size: Kirigami.Units.smallSpacing - 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) - } - - Behavior on color { - ColorAnimation { duration: Kirigami.Units.shortDuration } - } - } - } + showBackground: root.cardBackground && !Config.compactLayout } background: Rectangle { @@ -542,9 +429,9 @@ TimelineDelegate { ReactionDelegate { Layout.maximumWidth: root.width - 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 + Layout.alignment: _private.showUserMessageOnRight ? Qt.AlignRight : Qt.AlignLeft + Layout.leftMargin: _private.showUserMessageOnRight ? 0 : bubble.x + bubble.anchors.leftMargin + Layout.rightMargin: _private.showUserMessageOnRight ? Kirigami.Units.largeSpacing : 0 visible: root.showReactions model: root.reaction @@ -581,4 +468,13 @@ TimelineDelegate { ListView.view.setHoverActionsToDelegate(root) } } + + QtObject { + id: _private + + /** + * @brief Whether local user messages should be aligned right. + */ + property bool showUserMessageOnRight: Config.showLocalMessagesOnRight && root.author.isLocalUser && !Config.compactLayout && !root.alwaysMaxWidth + } } diff --git a/src/qml/Component/Timeline/PollDelegate.qml b/src/qml/Component/Timeline/PollDelegate.qml index b2c848ad9..4aebb5bd7 100644 --- a/src/qml/Component/Timeline/PollDelegate.qml +++ b/src/qml/Component/Timeline/PollDelegate.qml @@ -29,7 +29,7 @@ MessageDelegate { */ property var pollHandler: currentRoom.poll(root.eventId) - innerObject: ColumnLayout { + bubbleContent: ColumnLayout { Label { id: questionLabel text: root.content["org.matrix.msc3381.poll.start"]["question"]["body"] @@ -37,7 +37,6 @@ MessageDelegate { Repeater { model: root.content["org.matrix.msc3381.poll.start"]["answers"] delegate: RowLayout { - width: root.innerObject.width CheckBox { checked: root.pollHandler.answers[currentRoom.localUser.id] ? root.pollHandler.answers[currentRoom.localUser.id].includes(modelData["id"]) : false onClicked: root.pollHandler.sendPollAnswer(root.eventId, modelData["id"]) diff --git a/src/qml/Component/Timeline/ReplyComponent.qml b/src/qml/Component/Timeline/ReplyComponent.qml index 522443988..2518f5212 100644 --- a/src/qml/Component/Timeline/ReplyComponent.qml +++ b/src/qml/Component/Timeline/ReplyComponent.qml @@ -67,7 +67,7 @@ Item { */ required property var mediaInfo - required property real contentMaxWidth + property real contentMaxWidth /** * @brief The reply has been clicked. @@ -79,8 +79,8 @@ Item { GridLayout { id: mainLayout - anchors.fill: parent + implicitHeight: Math.max(replyAvatar.implicitHeight, replyName.implicitHeight) + loader.implicitHeight rows: 2 columns: 3 @@ -107,6 +107,7 @@ Item { color: root.author.color } QQC2.Label { + id: replyName Layout.fillWidth: true color: root.author.color @@ -117,7 +118,7 @@ Item { id: loader Layout.fillWidth: true - Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : -1 + Layout.maximumHeight: loader.item && (root.type == DelegateType.Image || root.type == DelegateType.Sticker) ? loader.item.height : loader.item.implicitHeight Layout.columnSpan: 2 sourceComponent: { @@ -152,7 +153,6 @@ Item { id: textComponent RichLabel { textMessage: root.display - textFormat: Text.RichText HoverHandler { enabled: !hoveredLink diff --git a/src/qml/Component/Timeline/TextDelegate.qml b/src/qml/Component/Timeline/TextDelegate.qml index 4293d7d98..963f068fc 100644 --- a/src/qml/Component/Timeline/TextDelegate.qml +++ b/src/qml/Component/Timeline/TextDelegate.qml @@ -38,8 +38,7 @@ MessageDelegate { onOpenContextMenu: RoomManager.viewEventMenu(eventId, author, delegateType, plainText, display, label.selectedText) - innerObject: ColumnLayout { - Layout.maximumWidth: root.contentMaxWidth + bubbleContent: ColumnLayout { RichLabel { id: label Layout.fillWidth: true diff --git a/src/qml/Component/Timeline/VideoDelegate.qml b/src/qml/Component/Timeline/VideoDelegate.qml index d1775ab9f..1b0899947 100644 --- a/src/qml/Component/Timeline/VideoDelegate.qml +++ b/src/qml/Component/Timeline/VideoDelegate.qml @@ -67,10 +67,10 @@ MessageDelegate { } } - innerObject: Video { + bubbleContent: Video { id: vid - Layout.preferredWidth: mediaSizeHelper.currentSize.width - Layout.preferredHeight: mediaSizeHelper.currentSize.height + implicitWidth: mediaSizeHelper.currentSize.width + implicitHeight: mediaSizeHelper.currentSize.height fillMode: VideoOutput.PreserveAspectFit diff --git a/src/qml/Component/TimelineView.qml b/src/qml/Component/TimelineView.qml index e2da4f00f..f2ba23a90 100644 --- a/src/qml/Component/TimelineView.qml +++ b/src/qml/Component/TimelineView.qml @@ -43,10 +43,7 @@ QQC2.ScrollView { // This is because itemAt returns null in the spaces. // All spacing should be handled by the delegates themselves spacing: 0 - // Ensures that the top item is not covered by sectionBanner if the page is scrolled all the way up - // topMargin: sectionBanner.height verticalLayoutDirection: ListView.BottomToTop - highlightMoveDuration: 500 clip: true interactive: Kirigami.Settings.isMobile bottomMargin: Kirigami.Units.largeSpacing + Math.round(Kirigami.Theme.defaultFont.pointSize * 2) diff --git a/src/res.qrc b/src/res.qrc index 5231339a1..f9656db92 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -49,6 +49,7 @@ qml/Component/Timeline/StateDelegate.qml qml/Component/Timeline/RichLabel.qml qml/Component/Timeline/MessageDelegate.qml + qml/Component/Timeline/Bubble.qml qml/Component/Timeline/SectionDelegate.qml qml/Component/Timeline/VideoDelegate.qml qml/Component/Timeline/ReactionDelegate.qml