Use MessageContentModel for replies
This allows code and other components to be displayed nicely.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
91
src/timeline/LinkPreviewLoadComponent.qml
Normal file
91
src/timeline/LinkPreviewLoadComponent.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
170
src/timeline/ReplyMessageComponentChooser.qml
Normal file
170
src/timeline/ReplyMessageComponentChooser.qml
Normal 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 {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user