Refactor threads

The focus here is to make threads use the standard message content system rather than having a special implementation.

To achieve this the threadroot content model will now get a thread body component which will visualise the thread model with all the other messages. The latest message in the thread will then just ask for the thread root content model and show that.

Note: in order to stop a cyclical dependency with MessageComponentChooser and new base version has been added which is just missing ThreadBodyComponent and and the main version is now inherited from that with ThreadBodyComponent added.
This commit is contained in:
James Graham
2025-01-17 19:18:44 +00:00
parent 111a45ab38
commit 7a949dccbb
12 changed files with 434 additions and 268 deletions

View File

@@ -0,0 +1,240 @@
// 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 Qt.labs.qmlmodels
import org.kde.neochat
/**
* @brief Select a message component based on a MessageComponentType.
*/
DelegateChooser {
id: root
/**
* @brief The NeoChatRoom the delegate is being displayed in.
*/
required property NeoChatRoom room
/**
* @brief The index of the delegate in the model.
*/
required property var index
/**
* @brief The timeline ListView this component is being used in.
*/
required property ListView timeline
/**
* @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)
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
/**
* @brief The user hovered link has changed.
*/
signal hoveredLinkChanged(string hoveredLink)
/**
* @brief Request a context menu be show for the message.
*/
signal showMessageMenu
signal removeLinkPreview(int index)
role: "componentType"
DelegateChoice {
roleValue: MessageComponentType.Author
delegate: AuthorComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Text
delegate: TextComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: root.selectedTextChanged(selectedText)
onHoveredLinkChanged: root.hoveredLinkChanged(hoveredLink)
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Image
delegate: ImageComponent {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Video
delegate: VideoComponent {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Code
delegate: CodeComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Quote
delegate: QuoteComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Audio
delegate: AudioComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.File
delegate: FileComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Itinerary
delegate: ItineraryComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Pdf
delegate: PdfPreviewComponent {
maxContentWidth: root.maxContentWidth
}
}
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.Reply
delegate: ReplyComponent {
maxContentWidth: root.maxContentWidth
onReplyClicked: eventId => {
root.replyClicked(eventId);
}
}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreview
delegate: LinkPreviewComponent {
maxContentWidth: root.maxContentWidth
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreviewLoad
delegate: LinkPreviewLoadComponent {
type: LinkPreviewLoadComponent.LinkPreview
maxContentWidth: root.maxContentWidth
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.ChatBar
delegate: ChatBarComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.ReplyButton
delegate: ReplyButtonComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Verification
delegate: MimeComponent {
mimeIconSource: "security-high"
label: i18n("%1 started a user verification", model.author.htmlSafeDisplayName)
}
}
DelegateChoice {
roleValue: MessageComponentType.Loading
delegate: LoadComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Other
delegate: Item {}
}
}

View File

@@ -19,6 +19,7 @@ ecm_add_qml_module(timeline GENERATE_PLUGIN_SOURCE
AvatarFlow.qml
ReactionDelegate.qml
SectionDelegate.qml
BaseMessageComponentChooser.qml
MessageComponentChooser.qml
ReplyMessageComponentChooser.qml
AuthorComponent.qml
@@ -50,6 +51,7 @@ ecm_add_qml_module(timeline GENERATE_PLUGIN_SOURCE
ReplyComponent.qml
StateComponent.qml
TextComponent.qml
ThreadBodyComponent.qml
VideoComponent.qml
SOURCES
timelinedelegate.cpp

View File

@@ -9,232 +9,16 @@ import org.kde.neochat
/**
* @brief Select a message component based on a MessageComponentType.
*/
DelegateChooser {
BaseMessageComponentChooser {
id: root
/**
* @brief The NeoChatRoom the delegate is being displayed in.
*/
required property NeoChatRoom room
/**
* @brief The index of the delegate in the model.
*/
required property var index
/**
* @brief The timeline ListView this component is being used in.
*/
required property ListView timeline
/**
* @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)
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
/**
* @brief The user hovered link has changed.
*/
signal hoveredLinkChanged(string hoveredLink)
/**
* @brief Request a context menu be show for the message.
*/
signal showMessageMenu
signal removeLinkPreview(int index)
role: "componentType"
DelegateChoice {
roleValue: MessageComponentType.Author
delegate: AuthorComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Text
delegate: TextComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: root.selectedTextChanged(selectedText)
onHoveredLinkChanged: root.hoveredLinkChanged(hoveredLink)
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Image
delegate: ImageComponent {
roleValue: MessageComponentType.ThreadBody
delegate: ThreadBodyComponent {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Video
delegate: VideoComponent {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Code
delegate: CodeComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Quote
delegate: QuoteComponent {
maxContentWidth: root.maxContentWidth
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onShowMessageMenu: root.showMessageMenu()
}
}
DelegateChoice {
roleValue: MessageComponentType.Audio
delegate: AudioComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.File
delegate: FileComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Itinerary
delegate: ItineraryComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Pdf
delegate: PdfPreviewComponent {
maxContentWidth: root.maxContentWidth
}
}
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.Reply
delegate: ReplyComponent {
maxContentWidth: root.maxContentWidth
onReplyClicked: eventId => {
root.replyClicked(eventId);
}
}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreview
delegate: LinkPreviewComponent {
maxContentWidth: root.maxContentWidth
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.LinkPreviewLoad
delegate: LinkPreviewLoadComponent {
type: LinkPreviewLoadComponent.LinkPreview
maxContentWidth: root.maxContentWidth
onRemove: index => root.removeLinkPreview(index)
}
}
DelegateChoice {
roleValue: MessageComponentType.ChatBar
delegate: ChatBarComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.ReplyButton
delegate: ReplyButtonComponent {
room: root.room
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Verification
delegate: MimeComponent {
mimeIconSource: "security-high"
label: i18n("%1 started a user verification", model.author.htmlSafeDisplayName)
}
}
DelegateChoice {
roleValue: MessageComponentType.Loading
delegate: LoadComponent {
maxContentWidth: root.maxContentWidth
}
}
DelegateChoice {
roleValue: MessageComponentType.Other
delegate: Item {}
}
}

View File

@@ -300,13 +300,7 @@ TimelineDelegate {
showAuthor: root.showAuthor
isThreaded: root.isThreaded
// HACK: This is stupid but seemingly QConcatenateTablesProxyModel
// can't be passed as a model role, always returning null.
contentModel: if (root.isThreaded && NeoChatConfig.threads) {
return RoomManager.timelineModel.timelineMessageModel.threadModelForRootId(root.threadRoot);
} else {
return root.contentModel;
}
contentModel: root.contentModel
timeline: root.ListView.view
showHighlight: root.showHighlight

View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2025 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 org.kde.kirigami as Kirigami
import org.kde.neochat
/**
* @brief A component to visualize a ThreadModel.
*
* @sa ThreadModel
*/
ColumnLayout {
id: root
/**
* @brief The NeoChatRoom the delegate is being displayed in.
*/
required property NeoChatRoom room
/**
* @brief The index of the delegate in the model.
*/
required property var index
/**
* @brief The Matrix ID of the root message in the thread, if any.
*/
required property string threadRoot
/**
* @brief The timeline ListView this component is being used in.
*/
required property ListView timeline
/**
* @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)
/**
* @brief The user selected text has changed.
*/
signal selectedTextChanged(string selectedText)
/**
* @brief The user hovered link has changed.
*/
signal hoveredLinkChanged(string hoveredLink)
/**
* @brief Request a context menu be show for the message.
*/
signal showMessageMenu
Layout.fillWidth: true
Layout.fillHeight: true
Layout.maximumWidth: root.maxContentWidth
spacing: Kirigami.Units.smallSpacing
Repeater {
id: threadRepeater
model: root.room.modelForThread(root.threadRoot);
delegate: BaseMessageComponentChooser {
room: root.room
index: root.index
timeline: root.timeline
maxContentWidth: root.maxContentWidth
onReplyClicked: eventId => {
root.replyClicked(eventId);
}
onSelectedTextChanged: selectedText => {
root.selectedTextChanged(selectedText);
}
onHoveredLinkChanged: hoveredLink => {
root.hoveredLinkChanged(hoveredLink);
}
onShowMessageMenu: root.showMessageMenu()
onRemoveLinkPreview: index => threadRepeater.model.closeLinkPreview(index)
}
}
}