Thread fetch more button
Changes threads so there is a button to fetch more events. Also adds a separator between the thread root and the rest of the events.
This commit is contained in:
@@ -56,8 +56,10 @@ public:
|
|||||||
ThreadRoot, /**< The root message of the thread. */
|
ThreadRoot, /**< The root message of the thread. */
|
||||||
ThreadBody, /**< The other messages in the thread. */
|
ThreadBody, /**< The other messages in the thread. */
|
||||||
ReplyButton, /**< A button to reply in the current thread. */
|
ReplyButton, /**< A button to reply in the current thread. */
|
||||||
|
FetchButton, /**< A button to fetch more messages in the current thread. */
|
||||||
Verification, /**< A user verification session start message. */
|
Verification, /**< A user verification session start message. */
|
||||||
Loading, /**< The component is loading. */
|
Loading, /**< The component is loading. */
|
||||||
|
Separator, /**< A horizontal separator. */
|
||||||
Other, /**< Anything that cannot be classified as another type. */
|
Other, /**< Anything that cannot be classified as another type. */
|
||||||
};
|
};
|
||||||
Q_ENUM(Type);
|
Q_ENUM(Type);
|
||||||
|
|||||||
@@ -486,6 +486,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
|
|||||||
#else
|
#else
|
||||||
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
|
if (isThreading && roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->id() == roomMessageEvent->threadRootEventId()) {
|
||||||
#endif
|
#endif
|
||||||
|
newComponents += MessageComponent{MessageComponentType::Separator, {}, {}};
|
||||||
newComponents += MessageComponent{MessageComponentType::ThreadBody, u"Thread Body"_s, {}};
|
newComponents += MessageComponent{MessageComponentType::ThreadBody, u"Thread Body"_s, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
|
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
|
||||||
: QConcatenateTablesProxyModel(room)
|
: QConcatenateTablesProxyModel(room)
|
||||||
, m_threadRootId(threadRootId)
|
, m_threadRootId(threadRootId)
|
||||||
|
, m_threadFetchModel(new ThreadFetchModel(this))
|
||||||
, m_threadChatBarModel(new ThreadChatBarModel(this, room))
|
, m_threadChatBarModel(new ThreadChatBarModel(this, room))
|
||||||
{
|
{
|
||||||
Q_ASSERT(!m_threadRootId.isEmpty());
|
Q_ASSERT(!m_threadRootId.isEmpty());
|
||||||
@@ -48,7 +49,7 @@ ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
|
|||||||
// If the thread was created by the local user fetchMore() won't find the current
|
// If the thread was created by the local user fetchMore() won't find the current
|
||||||
// pending event.
|
// pending event.
|
||||||
checkPending();
|
checkPending();
|
||||||
fetchMore({});
|
fetchMoreEvents(3);
|
||||||
addModels();
|
addModels();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,19 +78,19 @@ QHash<int, QByteArray> ThreadModel::roleNames() const
|
|||||||
return MessageContentModel::roleNamesStatic();
|
return MessageContentModel::roleNamesStatic();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ThreadModel::canFetchMore(const QModelIndex &parent) const
|
bool ThreadModel::moreEventsAvailable(const QModelIndex &parent) const
|
||||||
{
|
{
|
||||||
Q_UNUSED(parent);
|
Q_UNUSED(parent);
|
||||||
return !m_currentJob && m_nextBatch.has_value();
|
return !m_currentJob && m_nextBatch.has_value();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThreadModel::fetchMore(const QModelIndex &parent)
|
void ThreadModel::fetchMoreEvents(int max)
|
||||||
{
|
{
|
||||||
Q_UNUSED(parent);
|
|
||||||
if (!m_currentJob && m_nextBatch.has_value()) {
|
if (!m_currentJob && m_nextBatch.has_value()) {
|
||||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||||
const auto connection = room->connection();
|
const auto connection = room->connection();
|
||||||
m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), 5);
|
m_currentJob = connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, u"m.thread"_s, *m_nextBatch, QString(), max);
|
||||||
|
Q_EMIT moreEventsAvailableChanged();
|
||||||
connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() {
|
connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() {
|
||||||
auto newEvents = m_currentJob->chunk();
|
auto newEvents = m_currentJob->chunk();
|
||||||
for (auto &event : newEvents) {
|
for (auto &event : newEvents) {
|
||||||
@@ -109,6 +110,7 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_currentJob.clear();
|
m_currentJob.clear();
|
||||||
|
Q_EMIT moreEventsAvailableChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,6 +134,7 @@ void ThreadModel::addModels()
|
|||||||
if (room == nullptr) {
|
if (room == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
addSourceModel(m_threadFetchModel);
|
||||||
for (auto it = m_events.crbegin(); it != m_events.crend(); ++it) {
|
for (auto it = m_events.crbegin(); it != m_events.crend(); ++it) {
|
||||||
const auto contentModel = room->contentModelForEvent(*it);
|
const auto contentModel = room->contentModelForEvent(*it);
|
||||||
if (contentModel != nullptr) {
|
if (contentModel != nullptr) {
|
||||||
@@ -150,6 +153,7 @@ void ThreadModel::clearModels()
|
|||||||
if (room == nullptr) {
|
if (room == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
removeSourceModel(m_threadFetchModel);
|
||||||
for (const auto &model : m_events) {
|
for (const auto &model : m_events) {
|
||||||
const auto contentModel = room->contentModelForEvent(model);
|
const auto contentModel = room->contentModelForEvent(model);
|
||||||
if (sourceModels().contains(contentModel)) {
|
if (sourceModels().contains(contentModel)) {
|
||||||
@@ -188,6 +192,47 @@ void ThreadModel::closeLinkPreview(int row)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ThreadFetchModel::ThreadFetchModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
{
|
||||||
|
const auto threadModel = dynamic_cast<ThreadModel *>(parent);
|
||||||
|
Q_ASSERT(threadModel != nullptr);
|
||||||
|
connect(threadModel, &ThreadModel::moreEventsAvailableChanged, this, [this]() {
|
||||||
|
beginResetModel();
|
||||||
|
endResetModel();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ThreadFetchModel::data(const QModelIndex &idx, int role) const
|
||||||
|
{
|
||||||
|
if (idx.row() < 0 || idx.row() > 1) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == ComponentTypeRole) {
|
||||||
|
return MessageComponentType::FetchButton;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int ThreadFetchModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
Q_UNUSED(parent)
|
||||||
|
const auto threadModel = dynamic_cast<ThreadModel *>(this->parent());
|
||||||
|
if (threadModel == nullptr) {
|
||||||
|
qWarning() << "ThreadFetchModel created with incorrect parent, a ThreadModel must be set as the parent on creation.";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return threadModel->moreEventsAvailable({}) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> ThreadFetchModel::roleNames() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{ComponentTypeRole, "componentType"},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
ThreadChatBarModel::ThreadChatBarModel(QObject *parent, NeoChatRoom *room)
|
ThreadChatBarModel::ThreadChatBarModel(QObject *parent, NeoChatRoom *room)
|
||||||
: QAbstractListModel(parent)
|
: QAbstractListModel(parent)
|
||||||
, m_room(room)
|
, m_room(room)
|
||||||
|
|||||||
@@ -21,6 +21,52 @@
|
|||||||
class NeoChatRoom;
|
class NeoChatRoom;
|
||||||
class ReactionModel;
|
class ReactionModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class ThreadFetchModel
|
||||||
|
*
|
||||||
|
* A model to provide a fetch more historical messages button in a thread.
|
||||||
|
*/
|
||||||
|
class ThreadFetchModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Defines the model roles.
|
||||||
|
*
|
||||||
|
* The role values need to match MessageContentModel not to blow up.
|
||||||
|
*
|
||||||
|
* @sa MessageContentModel
|
||||||
|
*/
|
||||||
|
enum Roles {
|
||||||
|
ComponentTypeRole = MessageContentModel::ComponentTypeRole, /**< The type of component to visualise the message. */
|
||||||
|
};
|
||||||
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
|
explicit ThreadFetchModel(QObject *parent);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get the given role value at the given index.
|
||||||
|
*
|
||||||
|
* @sa QAbstractItemModel::data
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 1 or 0, depending on whether there are more messages to download.
|
||||||
|
*
|
||||||
|
* @sa QAbstractItemModel::rowCount
|
||||||
|
*/
|
||||||
|
[[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns a map with ComponentTypeRole it's the only one.
|
||||||
|
*
|
||||||
|
* @sa Roles, QAbstractItemModel::roleNames()
|
||||||
|
*/
|
||||||
|
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class ThreadChatBarModel
|
* @class ThreadChatBarModel
|
||||||
*
|
*
|
||||||
@@ -99,18 +145,14 @@ public:
|
|||||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Whether there is more data available for the model to fetch.
|
* @brief Whether there are more events for the model to fetch.
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::canFetchMore()
|
|
||||||
*/
|
*/
|
||||||
bool canFetchMore(const QModelIndex &parent) const override;
|
bool moreEventsAvailable(const QModelIndex &parent) const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Fetches the next batch of model data if any is available.
|
* @brief Fetches the next batch of events if any is available.
|
||||||
*
|
|
||||||
* @sa QAbstractItemModel::fetchMore()
|
|
||||||
*/
|
*/
|
||||||
void fetchMore(const QModelIndex &parent) override;
|
Q_INVOKABLE void fetchMoreEvents(int max = 5);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Close the link preview at the given index.
|
* @brief Close the link preview at the given index.
|
||||||
@@ -119,11 +161,15 @@ public:
|
|||||||
*/
|
*/
|
||||||
Q_INVOKABLE void closeLinkPreview(int row);
|
Q_INVOKABLE void closeLinkPreview(int row);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void moreEventsAvailableChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_threadRootId;
|
QString m_threadRootId;
|
||||||
QPointer<MessageContentModel> m_threadRootContentModel;
|
QPointer<MessageContentModel> m_threadRootContentModel;
|
||||||
|
|
||||||
std::deque<QString> m_events;
|
std::deque<QString> m_events;
|
||||||
|
ThreadFetchModel *m_threadFetchModel;
|
||||||
ThreadChatBarModel *m_threadChatBarModel;
|
ThreadChatBarModel *m_threadChatBarModel;
|
||||||
|
|
||||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
import Qt.labs.qmlmodels
|
import Qt.labs.qmlmodels
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
|
||||||
import org.kde.neochat
|
import org.kde.neochat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,6 +57,11 @@ DelegateChooser {
|
|||||||
|
|
||||||
signal removeLinkPreview(int index)
|
signal removeLinkPreview(int index)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request more events in the thread be loaded.
|
||||||
|
*/
|
||||||
|
signal fetchMoreEvents()
|
||||||
|
|
||||||
role: "componentType"
|
role: "componentType"
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
@@ -218,6 +226,14 @@ DelegateChooser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.FetchButton
|
||||||
|
delegate: FetchButtonComponent {
|
||||||
|
maxContentWidth: root.maxContentWidth
|
||||||
|
onFetchMoreEvents: root.fetchMoreEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageComponentType.Verification
|
roleValue: MessageComponentType.Verification
|
||||||
delegate: MimeComponent {
|
delegate: MimeComponent {
|
||||||
@@ -233,6 +249,14 @@ DelegateChooser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DelegateChoice {
|
||||||
|
roleValue: MessageComponentType.Separator
|
||||||
|
delegate: Kirigami.Separator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DelegateChoice {
|
DelegateChoice {
|
||||||
roleValue: MessageComponentType.Other
|
roleValue: MessageComponentType.Other
|
||||||
delegate: Item {}
|
delegate: Item {}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ ecm_add_qml_module(timeline GENERATE_PLUGIN_SOURCE
|
|||||||
ChatBarComponent.qml
|
ChatBarComponent.qml
|
||||||
CodeComponent.qml
|
CodeComponent.qml
|
||||||
EncryptedComponent.qml
|
EncryptedComponent.qml
|
||||||
|
FetchButtonComponent.qml
|
||||||
FileComponent.qml
|
FileComponent.qml
|
||||||
ImageComponent.qml
|
ImageComponent.qml
|
||||||
ItineraryComponent.qml
|
ItineraryComponent.qml
|
||||||
|
|||||||
52
src/timeline/FetchButtonComponent.qml
Normal file
52
src/timeline/FetchButtonComponent.qml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// 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.Controls as QQC2
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
import org.kde.kirigami as Kirigami
|
||||||
|
import org.kde.kirigamiaddons.delegates as Delegates
|
||||||
|
|
||||||
|
import org.kde.neochat
|
||||||
|
import org.kde.neochat.chatbar
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief A component to show a reply button for threads in a message bubble.
|
||||||
|
*/
|
||||||
|
Delegates.RoundedItemDelegate {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief The maximum width that the bubble's content can be.
|
||||||
|
*/
|
||||||
|
property real maxContentWidth: -1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request more events in the thread be loaded.
|
||||||
|
*/
|
||||||
|
signal fetchMoreEvents()
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumWidth: root.maxContentWidth
|
||||||
|
|
||||||
|
leftInset: 0
|
||||||
|
rightInset: 0
|
||||||
|
|
||||||
|
highlighted: true
|
||||||
|
|
||||||
|
icon.name: "arrow-up"
|
||||||
|
icon.width: Kirigami.Units.iconSizes.sizeForLabels
|
||||||
|
icon.height: Kirigami.Units.iconSizes.sizeForLabels
|
||||||
|
text: i18nc("@action:button", "Fetch More Events")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.fetchMoreEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Kirigami.Icon {
|
||||||
|
implicitWidth: root.icon.width
|
||||||
|
implicitHeight: root.icon.height
|
||||||
|
source: root.icon.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,6 +87,7 @@ ColumnLayout {
|
|||||||
}
|
}
|
||||||
onShowMessageMenu: root.showMessageMenu()
|
onShowMessageMenu: root.showMessageMenu()
|
||||||
onRemoveLinkPreview: index => threadRepeater.model.closeLinkPreview(index)
|
onRemoveLinkPreview: index => threadRepeater.model.closeLinkPreview(index)
|
||||||
|
onFetchMoreEvents: threadRepeater.model.fetchMoreEvents(5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user