Use MessageContentModel for replies

This allows code and other components to be displayed nicely.
This commit is contained in:
James Graham
2024-05-27 14:54:42 +00:00
parent 3615c3e8e5
commit efb72652ce
14 changed files with 536 additions and 301 deletions

View File

@@ -18,6 +18,7 @@ qt_add_qml_module(timeline
ReactionDelegate.qml
SectionDelegate.qml
MessageComponentChooser.qml
ReplyMessageComponentChooser.qml
AudioComponent.qml
CodeComponent.qml
EncryptedComponent.qml
@@ -32,6 +33,7 @@ qt_add_qml_module(timeline
FlightReservationComponent.qml
HotelReservationComponent.qml
LinkPreviewComponent.qml
LinkPreviewLoadComponent.qml
LiveLocationComponent.qml
LoadComponent.qml
LocationComponent.qml

View File

@@ -34,7 +34,7 @@ QQC2.Control {
/**
* @brief The timestamp of the message.
*/
required property var time
property date time
/**
* @brief The display text of the message.
@@ -135,6 +135,7 @@ QQC2.Control {
}
TapHandler {
enabled: root.time.toString() !== "Invalid Date"
acceptedButtons: Qt.LeftButton
onTapped: RoomManager.maximizeCode(root.author, root.time, root.display, root.componentAttributes.class)
onLongPressed: root.showMessageMenu()
@@ -174,6 +175,7 @@ QQC2.Control {
QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay
}
QQC2.Button {
visible: root.time.toString() !== "Invalid Date"
icon.name: "view-fullscreen"
text: i18nc("@action:button", "Maximize")
display: QQC2.AbstractButton.IconOnly

View File

@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
/**
* @brief A component to show a link preview loading from a message.
*/
QQC2.Control {
id: root
/**
* @brief The index of the delegate in the model.
*/
required property int index
required property int type
/**
* @brief Standard height for the link preview.
*
* When the content of the link preview is larger than this it will be
* elided/hidden until maximized.
*/
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
/**
* @brief The maximum width that the bubble's content can be.
*/
property real maxContentWidth: -1
/**
* @brief Request for this delegate to be removed.
*/
signal remove(int index)
enum Type {
Reply,
LinkPreview
}
Layout.fillWidth: true
Layout.maximumWidth: root.maxContentWidth
contentItem : RowLayout {
spacing: Kirigami.Units.smallSpacing
Rectangle {
Layout.fillHeight: true
width: Kirigami.Units.smallSpacing
color: Kirigami.Theme.highlightColor
}
QQC2.BusyIndicator {}
Kirigami.Heading {
Layout.fillWidth: true
Layout.minimumHeight: root.defaultHeight
verticalAlignment: Text.AlignVCenter
level: 2
text: {
switch (root.type) {
case LinkPreviewLoadComponent.Reply:
return i18n("Loading reply");
case LinkPreviewLoadComponent.LinkPreview:
return i18n("Loading URL preview");
}
}
}
}
QQC2.Button {
id: closeButton
anchors.right: parent.right
anchors.top: parent.top
visible: root.hovered && root.type === LinkPreviewLoadComponent.LinkPreview
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
icon.name: "dialog-close"
display: QQC2.AbstractButton.IconOnly
onClicked: root.remove(root.index)
QQC2.ToolTip {
text: closeButton.text
visible: closeButton.hovered
delay: Kirigami.Units.toolTipDelay
}
}
}

View File

@@ -8,84 +8,29 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
/**
* @brief A component to show a link preview loading from a message.
* @brief A component to show that part of a message is loading.
*/
QQC2.Control {
RowLayout {
id: root
/**
* @brief The index of the delegate in the model.
*/
required property int index
required property int type
/**
* @brief Standard height for the link preview.
*
* When the content of the link preview is larger than this it will be
* elided/hidden until maximized.
*/
property var defaultHeight: Kirigami.Units.gridUnit * 3 + Kirigami.Units.smallSpacing * 2
required property string display
/**
* @brief The maximum width that the bubble's content can be.
*/
property real maxContentWidth: -1
/**
* @brief Request for this delegate to be removed.
*/
signal remove(int index)
enum Type {
Reply,
LinkPreview
}
Layout.fillWidth: true
Layout.maximumWidth: root.maxContentWidth
spacing: Kirigami.Units.smallSpacing
contentItem : RowLayout {
spacing: Kirigami.Units.smallSpacing
Rectangle {
Layout.fillHeight: true
width: Kirigami.Units.smallSpacing
color: Kirigami.Theme.highlightColor
}
QQC2.BusyIndicator {}
Kirigami.Heading {
Layout.fillWidth: true
Layout.minimumHeight: root.defaultHeight
verticalAlignment: Text.AlignVCenter
level: 2
text: {
switch (root.type) {
case LoadComponent.Reply:
return i18n("Loading reply");
case LoadComponent.LinkPreview:
return i18n("Loading URL preview");
}
}
}
}
QQC2.Button {
id: closeButton
anchors.right: parent.right
anchors.top: parent.top
visible: root.hovered && root.type === LoadComponent.LinkPreview
text: i18nc("As in remove the link preview so it's no longer shown", "Remove preview")
icon.name: "dialog-close"
display: QQC2.AbstractButton.IconOnly
onClicked: root.remove(root.index)
QQC2.ToolTip {
text: closeButton.text
visible: closeButton.hovered
delay: Kirigami.Units.toolTipDelay
}
QQC2.BusyIndicator {}
Kirigami.Heading {
id: loadingText
Layout.fillWidth: true
verticalAlignment: Text.AlignVCenter
level: 2
text: root.display.length > 0 ? root.display : i18n("Loading")
}
}

View File

@@ -183,17 +183,6 @@ DelegateChooser {
onReplyClicked: eventId => {
root.replyClicked(eventId);
}
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
}
}
DelegateChoice {
roleValue: MessageComponentType.ReplyLoad
delegate: LoadComponent {
type: LoadComponent.Reply
maxContentWidth: root.maxContentWidth
}
}
@@ -207,8 +196,8 @@ DelegateChooser {
DelegateChoice {
roleValue: MessageComponentType.LinkPreviewLoad
delegate: LoadComponent {
type: LoadComponent.LinkPreview
delegate: LinkPreviewLoadComponent {
type: LinkPreviewLoadComponent.LinkPreview
maxContentWidth: root.maxContentWidth
onRemove: index => root.removeLinkPreview(index)
}
@@ -231,6 +220,13 @@ DelegateChooser {
}
}
DelegateChoice {
roleValue: MessageComponentType.Loading
delegate: LoadComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Other
delegate: Item {}

View File

@@ -24,11 +24,6 @@ import org.kde.neochat
RowLayout {
id: root
/**
* @brief The matrix ID of the reply event.
*/
required property var replyComponentType
/**
* @brief The matrix ID of the reply event.
*/
@@ -53,26 +48,9 @@ RowLayout {
required property var replyAuthor
/**
* @brief The display text of the message replied to.
* @brief The model to visualise the content 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
required property var replyContentModel
/**
* @brief The maximum width that the bubble's content can be.
@@ -84,12 +62,6 @@ RowLayout {
*/
signal replyClicked(string eventID)
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
implicitHeight: contentColumn.implicitHeight
spacing: Kirigami.Units.largeSpacing
Rectangle {
@@ -101,7 +73,6 @@ RowLayout {
}
ColumnLayout {
id: contentColumn
implicitHeight: headerRow.implicitHeight + (root.replyComponentType != MessageComponentType.Other ? contentRepeater.itemAt(0).implicitHeight + spacing : 0)
spacing: Kirigami.Units.smallSpacing
RowLayout {
@@ -131,75 +102,11 @@ RowLayout {
}
Repeater {
id: contentRepeater
model: [root.replyComponentType]
delegate: DelegateChooser {
role: "modelData"
model: root.replyContentModel
delegate: ReplyMessageComponentChooser {
maxContentWidth: _private.availableContentWidth
DelegateChoice {
roleValue: MessageComponentType.Text
delegate: TextComponent {
display: root.replyDisplay
maxContentWidth: _private.availableContentWidth
onSelectedTextChanged: root.selectedTextChanged(selectedText)
HoverHandler {
enabled: !hoveredLink
cursorShape: Qt.PointingHandCursor
}
TapHandler {
enabled: !hoveredLink
acceptedButtons: Qt.LeftButton
onTapped: root.replyClicked(root.replyEventId)
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Image
delegate: Image {
id: image
Layout.maximumWidth: mediaSizeHelper.currentSize.width
Layout.maximumHeight: mediaSizeHelper.currentSize.height
source: root?.replyMediaInfo.source ?? ""
MediaSizeHelper {
id: mediaSizeHelper
contentMaxWidth: _private.availableContentWidth
mediaWidth: root?.replyMediaInfo.width ?? -1
mediaHeight: root?.replyMediaInfo.height ?? -1
}
}
}
DelegateChoice {
roleValue: MessageComponentType.File
delegate: MimeComponent {
mimeIconSource: root.replyMediaInfo.mimeIcon
label: root.replyDisplay
subLabel: root.replyComponentType === DelegateType.File ? Format.formatByteSize(root.replyMediaInfo.size) : Format.formatDuration(root.replyMediaInfo.duration)
}
}
DelegateChoice {
roleValue: MessageComponentType.Video
delegate: MimeComponent {
mimeIconSource: root.replyMediaInfo.mimeIcon
label: root.replyDisplay
subLabel: root.replyComponentType === DelegateType.File ? Format.formatByteSize(root.replyMediaInfo.size) : Format.formatDuration(root.replyMediaInfo.duration)
}
}
DelegateChoice {
roleValue: MessageComponentType.Audio
delegate: MimeComponent {
mimeIconSource: root.replyMediaInfo.mimeIcon
label: root.replyDisplay
subLabel: root.replyComponentType === DelegateType.File ? Format.formatByteSize(root.replyMediaInfo.size) : Format.formatDuration(root.replyMediaInfo.duration)
}
}
DelegateChoice {
roleValue: MessageComponentType.Encrypted
delegate: TextComponent {
display: i18n("This message is encrypted and the sender has not shared the key with this device.")
}
}
onReplyClicked: root.replyClicked(root.replyEventId)
}
}
}

View File

@@ -0,0 +1,170 @@
// SPDX-FileCopyrightText: 2024 James Graham <james.h.graham@protonmail.com>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
import QtQuick
import QtQuick.Layouts
import Qt.labs.qmlmodels
import org.kde.neochat
/**
* @brief Select a message component based on a MessageComponentType.
*/
DelegateChooser {
id: root
/**
* @brief The maximum width that the bubble's content can be.
*/
property real maxContentWidth: -1
/**
* @brief The reply has been clicked.
*/
signal replyClicked()
role: "componentType"
DelegateChoice {
roleValue: MessageComponentType.Text
delegate: TextComponent {
maxContentWidth: root.maxContentWidth
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: root.replyClicked()
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Image
delegate: Image {
id: image
required property var mediaInfo
Layout.maximumWidth: mediaSizeHelper.currentSize.width
Layout.maximumHeight: mediaSizeHelper.currentSize.height
source: image.mediaInfo.source
MediaSizeHelper {
id: mediaSizeHelper
contentMaxWidth: root.maxContentWidth
mediaWidth: image.mediaInfo.width ?? 0
mediaHeight: image.mediaInfo.height ?? 0
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Video
delegate: MimeComponent {
required property string display
required property var mediaInfo
required property int componentType
mimeIconSource: mediaInfo.mimeIcon
label: display
subLabel: componentType === MessageComponentType.File ? Format.formatByteSize(mediaInfo.size) : Format.formatDuration(mediaInfo.duration)
}
}
DelegateChoice {
roleValue: MessageComponentType.Code
delegate: CodeComponent {
maxContentWidth: root.maxContentWidth
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: root.replyClicked()
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Quote
delegate: QuoteComponent {
maxContentWidth: root.maxContentWidth
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: root.replyClicked()
}
}
}
DelegateChoice {
roleValue: MessageComponentType.Audio
delegate: MimeComponent {
required property string display
required property var mediaInfo
required property int componentType
mimeIconSource: mediaInfo.mimeIcon
label: display
subLabel: componentType === MessageComponentType.File ? Format.formatByteSize(mediaInfo.size) : Format.formatDuration(mediaInfo.duration)
}
}
DelegateChoice {
roleValue: MessageComponentType.File
delegate: MimeComponent {
required property string display
required property var mediaInfo
required property int componentType
mimeIconSource: mediaInfo.mimeIcon
label: display
subLabel: componentType === MessageComponentType.File ? Format.formatByteSize(mediaInfo.size) : Format.formatDuration(mediaInfo.duration)
}
}
DelegateChoice {
roleValue: MessageComponentType.Poll
delegate: PollComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Location
delegate: LocationComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.LiveLocation
delegate: LiveLocationComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Encrypted
delegate: EncryptedComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Loading
delegate: LoadComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Other
delegate: Item {}
}
}