Thread View
So at the moment this remains behind the feature flag as this only adds a threadmodel and a basic visualisation. There is much more to come to get it ready for full release.
This commit is contained in:
@@ -75,7 +75,10 @@ void MessageContentModel::initializeModel()
|
||||
});
|
||||
|
||||
if (m_event == nullptr) {
|
||||
m_room->downloadEventFromServer(m_eventId);
|
||||
m_room->getEvent(m_eventId);
|
||||
if (m_event == nullptr) {
|
||||
m_room->downloadEventFromServer(m_eventId);
|
||||
}
|
||||
}
|
||||
|
||||
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
|
||||
@@ -148,6 +151,11 @@ void MessageContentModel::initializeModel()
|
||||
}
|
||||
});
|
||||
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() {
|
||||
updateReplyModel();
|
||||
resetModel();
|
||||
});
|
||||
|
||||
if (m_event != nullptr) {
|
||||
updateReplyModel();
|
||||
}
|
||||
@@ -168,6 +176,9 @@ void MessageContentModel::intiializeEvent(const Quotient::RoomEvent *event)
|
||||
// a pending event may not previously have had an event ID so update.
|
||||
if (m_eventId.isEmpty()) {
|
||||
m_eventId = m_event->id();
|
||||
if (m_eventId.isEmpty()) {
|
||||
m_eventId = m_event->transactionId();
|
||||
}
|
||||
}
|
||||
|
||||
auto senderId = m_event->senderId();
|
||||
@@ -417,12 +428,19 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
|
||||
|
||||
void MessageContentModel::updateReplyModel()
|
||||
{
|
||||
if (m_event == nullptr || m_replyModel != nullptr || m_isReply) {
|
||||
if (m_event == nullptr || m_isReply) {
|
||||
return;
|
||||
}
|
||||
|
||||
EventHandler eventHandler(m_room, m_event.get());
|
||||
if (!eventHandler.hasReply()) {
|
||||
if (!eventHandler.hasReply() || (eventHandler.isThreaded() && NeoChatConfig::self()->threads())) {
|
||||
if (m_replyModel) {
|
||||
delete m_replyModel;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_replyModel != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "neochatroommember.h"
|
||||
#include "readmarkermodel.h"
|
||||
#include "texthandler.h"
|
||||
#include "threadmodel.h"
|
||||
|
||||
using namespace Quotient;
|
||||
|
||||
@@ -71,6 +72,11 @@ MessageEventModel::MessageEventModel(QObject *parent)
|
||||
connect(this, &MessageEventModel::modelReset, this, [this]() {
|
||||
resetting = false;
|
||||
});
|
||||
|
||||
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
NeoChatRoom *MessageEventModel::room() const
|
||||
@@ -497,6 +503,10 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
if (eventHandler.isThreaded() && eventHandler.threadRoot() != eventHandler.getId() && NeoChatConfig::threads()) {
|
||||
return EventStatus::Hidden;
|
||||
}
|
||||
|
||||
return EventStatus::Normal;
|
||||
}
|
||||
|
||||
@@ -646,6 +656,11 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event)
|
||||
}
|
||||
}
|
||||
|
||||
const auto eventHandler = EventHandler(m_currentRoom, event);
|
||||
if (eventHandler.isThreaded() && !m_threadModels.contains(eventHandler.threadRoot())) {
|
||||
m_threadModels[eventHandler.threadRoot()] = QSharedPointer<ThreadModel>(new ThreadModel(eventHandler.threadRoot(), m_currentRoom));
|
||||
}
|
||||
|
||||
// ReadMarkerModel handles updates to add and remove markers, we only need to
|
||||
// handle adding and removing whole models here.
|
||||
if (m_readMarkerModels.contains(eventId)) {
|
||||
@@ -705,4 +720,9 @@ bool MessageEventModel::event(QEvent *event)
|
||||
return QObject::event(event);
|
||||
}
|
||||
|
||||
ThreadModel *MessageEventModel::threadModelForRootId(const QString &threadRootId) const
|
||||
{
|
||||
return m_threadModels[threadRootId].data();
|
||||
}
|
||||
|
||||
#include "moc_messageeventmodel.cpp"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "neochatroommember.h"
|
||||
#include "pollhandler.h"
|
||||
#include "readmarkermodel.h"
|
||||
#include "threadmodel.h"
|
||||
|
||||
class ReactionModel;
|
||||
|
||||
@@ -55,8 +56,8 @@ public:
|
||||
|
||||
ContentModelRole, /**< The MessageContentModel for the event. */
|
||||
|
||||
IsThreadedRole,
|
||||
ThreadRootRole,
|
||||
IsThreadedRole, /**< Whether the message is in a thread. */
|
||||
ThreadRootRole, /**< The Matrix ID of the thread root message, if any . */
|
||||
|
||||
ShowSectionRole, /**< Whether the section header should be shown. */
|
||||
|
||||
@@ -105,6 +106,8 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE [[nodiscard]] int eventIdToRow(const QString &eventID) const;
|
||||
|
||||
Q_INVOKABLE ThreadModel *threadModelForRootId(const QString &threadRootId) const;
|
||||
|
||||
protected:
|
||||
bool event(QEvent *event) override;
|
||||
|
||||
@@ -120,6 +123,7 @@ private:
|
||||
std::map<QString, std::unique_ptr<NeochatRoomMember>> m_memberObjects;
|
||||
std::map<QString, std::unique_ptr<MessageContentModel>> m_contentModels;
|
||||
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
|
||||
QMap<QString, QSharedPointer<ThreadModel>> m_threadModels;
|
||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||
|
||||
[[nodiscard]] int timelineBaseIndex() const;
|
||||
|
||||
129
src/models/threadmodel.cpp
Normal file
129
src/models/threadmodel.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#include "threadmodel.h"
|
||||
|
||||
#include <Quotient/csapi/relations.h>
|
||||
#include <Quotient/events/event.h>
|
||||
#include <Quotient/events/stickerevent.h>
|
||||
#include <Quotient/jobs/basejob.h>
|
||||
#include <Quotient/omittable.h>
|
||||
#include <memory>
|
||||
|
||||
#include "eventhandler.h"
|
||||
#include "messagecontentmodel.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
ThreadModel::ThreadModel(const QString &threadRootId, NeoChatRoom *room)
|
||||
: QConcatenateTablesProxyModel(room)
|
||||
, m_threadRootId(threadRootId)
|
||||
{
|
||||
Q_ASSERT(!m_threadRootId.isEmpty());
|
||||
Q_ASSERT(room);
|
||||
|
||||
m_threadRootContentModel = std::unique_ptr<MessageContentModel>(new MessageContentModel(room, threadRootId));
|
||||
|
||||
connect(room, &Quotient::Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) {
|
||||
if (auto roomEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
|
||||
EventHandler eventHandler(dynamic_cast<NeoChatRoom *>(QObject::parent()), roomEvent);
|
||||
if (eventHandler.isThreaded() && eventHandler.threadRoot() == m_threadRootId) {
|
||||
addNewEvent(event);
|
||||
clearModels();
|
||||
addModels();
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(room, &Quotient::Room::aboutToAddNewMessages, this, [this](Quotient::RoomEventsRange events) {
|
||||
for (const auto &event : events) {
|
||||
if (auto roomEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
|
||||
EventHandler eventHandler(dynamic_cast<NeoChatRoom *>(QObject::parent()), roomEvent);
|
||||
if (eventHandler.isThreaded() && eventHandler.threadRoot() == m_threadRootId) {
|
||||
addNewEvent(roomEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
clearModels();
|
||||
addModels();
|
||||
});
|
||||
|
||||
fetchMore({});
|
||||
addModels();
|
||||
}
|
||||
|
||||
MessageContentModel *ThreadModel::threadRootContentModel() const
|
||||
{
|
||||
return m_threadRootContentModel.get();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> ThreadModel::roleNames() const
|
||||
{
|
||||
return m_threadRootContentModel->roleNames();
|
||||
}
|
||||
|
||||
bool ThreadModel::canFetchMore(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return !m_currentJob && m_nextBatch.has_value();
|
||||
}
|
||||
|
||||
void ThreadModel::fetchMore(const QModelIndex &parent)
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
if (!m_currentJob && m_nextBatch.has_value()) {
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
const auto connection = room->connection();
|
||||
m_currentJob =
|
||||
connection->callApi<Quotient::GetRelatingEventsWithRelTypeJob>(room->id(), m_threadRootId, QLatin1String("m.thread"), *m_nextBatch, QString(), 5);
|
||||
connect(m_currentJob, &Quotient::BaseJob::success, this, [this]() {
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
auto newEvents = m_currentJob->chunk();
|
||||
for (auto &event : newEvents) {
|
||||
m_contentModels.push_back(new MessageContentModel(room, event.get()));
|
||||
}
|
||||
|
||||
clearModels();
|
||||
addModels();
|
||||
|
||||
const auto newNextBatch = m_currentJob->nextBatch();
|
||||
if (!newNextBatch.isEmpty() && *m_nextBatch != newNextBatch) {
|
||||
*m_nextBatch = newNextBatch;
|
||||
} else {
|
||||
// Insert the thread root at the end.
|
||||
beginInsertRows({}, rowCount(), rowCount());
|
||||
endInsertRows();
|
||||
m_nextBatch.reset();
|
||||
}
|
||||
|
||||
m_currentJob.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadModel::addNewEvent(const Quotient::RoomEvent *event)
|
||||
{
|
||||
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
|
||||
m_contentModels.push_front(new MessageContentModel(room, event));
|
||||
}
|
||||
|
||||
void ThreadModel::addModels()
|
||||
{
|
||||
addSourceModel(m_threadRootContentModel.get());
|
||||
for (auto it = m_contentModels.crbegin(); it != m_contentModels.crend(); ++it) {
|
||||
addSourceModel(*it);
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void ThreadModel::clearModels()
|
||||
{
|
||||
removeSourceModel(m_threadRootContentModel.get());
|
||||
for (const auto &model : m_contentModels) {
|
||||
if (sourceModels().contains(model)) {
|
||||
removeSourceModel(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#include "moc_threadmodel.cpp"
|
||||
88
src/models/threadmodel.h
Normal file
88
src/models/threadmodel.h
Normal file
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: 2023 James Graham <james.h.graham@protonmail.com>
|
||||
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/util.h>
|
||||
|
||||
#include <QConcatenateTablesProxyModel>
|
||||
#include <QQmlEngine>
|
||||
|
||||
#include <QPointer>
|
||||
#include <Quotient/csapi/relations.h>
|
||||
#include <Quotient/events/roomevent.h>
|
||||
#include <Quotient/events/roommessageevent.h>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
|
||||
#include "linkpreviewer.h"
|
||||
#include "messagecontentmodel.h"
|
||||
|
||||
class NeoChatRoom;
|
||||
class ReactionModel;
|
||||
|
||||
/**
|
||||
* @class ThreadModel
|
||||
*
|
||||
* This class defines the model for visualising a thread.
|
||||
*
|
||||
* The class also provides functions to access the data of the root event, typically
|
||||
* used to visualise the thread in a list of room threads.
|
||||
*/
|
||||
class ThreadModel : public QConcatenateTablesProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE("")
|
||||
|
||||
public:
|
||||
explicit ThreadModel(const QString &threadRootId, NeoChatRoom *room);
|
||||
|
||||
/**
|
||||
* @brief The content model for the thread root event.
|
||||
*/
|
||||
MessageContentModel *threadRootContentModel() const;
|
||||
|
||||
/**
|
||||
* @brief Returns a mapping from Role enum values to role names.
|
||||
*
|
||||
* @sa Roles, QAbstractItemModel::roleNames()
|
||||
*/
|
||||
[[nodiscard]] QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
/**
|
||||
* @brief Whether there is more data available for the model to fetch.
|
||||
*
|
||||
* @sa QAbstractItemModel::canFetchMore()
|
||||
*/
|
||||
bool canFetchMore(const QModelIndex &parent) const override;
|
||||
|
||||
/**
|
||||
* @brief Fetches the next batch of model data if any is available.
|
||||
*
|
||||
* @sa QAbstractItemModel::fetchMore()
|
||||
*/
|
||||
void fetchMore(const QModelIndex &parent) override;
|
||||
|
||||
private:
|
||||
QString m_threadRootId;
|
||||
|
||||
std::unique_ptr<MessageContentModel> m_threadRootContentModel;
|
||||
|
||||
std::deque<MessageContentModel *> m_contentModels;
|
||||
|
||||
QList<QString> m_events;
|
||||
QList<QString> m_pendingEvents;
|
||||
|
||||
std::unordered_map<QString, std::unique_ptr<Quotient::RoomEvent>> m_unloadedEvents;
|
||||
|
||||
QMap<QString, QSharedPointer<ReactionModel>> m_reactionModels;
|
||||
|
||||
QPointer<Quotient::GetRelatingEventsWithRelTypeJob> m_currentJob = nullptr;
|
||||
std::optional<QString> m_nextBatch = QString();
|
||||
bool m_addingPending = false;
|
||||
|
||||
void addNewEvent(const Quotient::RoomEvent *event);
|
||||
void addModels();
|
||||
void clearModels();
|
||||
};
|
||||
Reference in New Issue
Block a user