Only save eventId in MessageContentModel

Turns out trying to manage pointers in the model is a bad idea so only save eventId in MessageContentModel, events pointers will now only be obtained temporarily then discarded to avoid both creating additional copies of the event in the model and potential sources of crashes.

This also creates a basic unit test that we can add to going forward.
This commit is contained in:
James Graham
2024-09-15 08:28:46 +00:00
parent e0c3b7f808
commit ec6a8dd028
8 changed files with 299 additions and 210 deletions

View File

@@ -82,3 +82,9 @@ ecm_add_test(
LINK_LIBRARIES neochat Qt::Test LINK_LIBRARIES neochat Qt::Test
TEST_NAME linkpreviewertest TEST_NAME linkpreviewertest
) )
ecm_add_test(
messagecontentmodeltest.cpp
LINK_LIBRARIES neochat Qt::Test
TEST_NAME messagecontentmodeltest
)

View File

@@ -0,0 +1,61 @@
// 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
#include <QObject>
#include <QSignalSpy>
#include <QTest>
#include <Quotient/connection.h>
#include <Quotient/quotient_common.h>
#include <Quotient/roommember.h>
#include <Quotient/syncdata.h>
#include "models/messagecontentmodel.h"
#include "testutils.h"
using namespace Quotient;
using namespace Qt::Literals::StringLiterals;
class MessageContentModelTest : public QObject
{
Q_OBJECT
private:
Connection *connection = nullptr;
private Q_SLOTS:
void initTestCase();
void missingEvent();
};
void MessageContentModelTest::initTestCase()
{
connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org"));
}
void MessageContentModelTest::missingEvent()
{
auto room = new TestUtils::TestRoom(connection, QStringLiteral("#firstRoom:kde.org"));
auto model1 = MessageContentModel(room, "$153456789:example.org"_L1);
QCOMPARE(model1.rowCount(), 1);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::DisplayRole), "Loading"_L1);
auto model2 = MessageContentModel(room, "$153456789:example.org"_L1, true);
QCOMPARE(model2.rowCount(), 1);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Loading);
QCOMPARE(model2.data(model2.index(0), MessageContentModel::DisplayRole), "Loading reply"_L1);
room->syncNewEvents(QLatin1String("test-min-sync.json"));
QCOMPARE(model1.rowCount(), 2);
QCOMPARE(model1.data(model1.index(0), MessageContentModel::ComponentTypeRole), MessageComponentType::Author);
QCOMPARE(model1.data(model1.index(1), MessageContentModel::ComponentTypeRole), MessageComponentType::Text);
QCOMPARE(model1.data(model1.index(1), MessageContentModel::DisplayRole), u"<b>This is an example<br>text message</b>"_s);
}
QTEST_MAIN(MessageContentModelTest)
#include "messagecontentmodeltest.moc"

View File

@@ -29,18 +29,6 @@
using namespace Quotient; using namespace Quotient;
MessageContentModel::MessageContentModel(NeoChatRoom *room, const Quotient::RoomEvent *event, bool isReply, bool isPending, MessageContentModel *parent)
: QAbstractListModel(parent)
, m_room(room)
, m_eventId(event != nullptr ? event->id() : QString())
, m_eventSenderId(event != nullptr ? event->senderId() : QString())
, m_isPending(isPending)
, m_isReply(isReply)
{
intiializeEvent(event);
initializeModel();
}
MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent) MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply, bool isPending, MessageContentModel *parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_room(room) , m_room(room)
@@ -54,17 +42,137 @@ MessageContentModel::MessageContentModel(NeoChatRoom *room, const QString &event
void MessageContentModel::initializeModel() void MessageContentModel::initializeModel()
{ {
Q_ASSERT(m_room != nullptr); Q_ASSERT(m_room != nullptr);
// Allow making a model for an event that is being downloaded but will appear later Q_ASSERT(!m_eventId.isEmpty());
// e.g. a reply, but we need an ID to know when it has arrived.
// Also note that a pending event may not have an event ID yet but as long as we have an event
// pointer we can pass out the transaction ID until it is set.
Q_ASSERT(!m_eventId.isEmpty() || m_event != nullptr);
connect(this, &MessageContentModel::eventUnavailable, this, &MessageContentModel::getEvent);
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
if (m_room != nullptr) {
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
beginResetModel();
m_isPending = false;
m_eventId = serverEvent->id();
initializeEvent();
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::addedMessages, this, [this](int fromIndex, int toIndex) {
if (m_room != nullptr) {
for (int i = fromIndex; i <= toIndex; i++) {
if (m_room->findInTimeline(i)->event()->id() == m_eventId) {
initializeEvent();
updateReplyModel();
resetModel();
}
}
}
});
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
if (m_room != nullptr) {
if (m_eventId == newEvent->id()) {
beginResetModel();
initializeEvent();
resetContent();
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_room != nullptr && eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
if (eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
if (oldEventId == m_eventId || newEventId == m_eventId) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
resetContent(newEventId == m_eventId);
endResetModel();
}
});
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
if (oldThreadId == m_eventId || newThreadId == m_eventId) {
beginResetModel();
resetContent(false, newThreadId == m_eventId);
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
resetContent();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this]() {
resetContent();
});
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
if (m_room != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
if (m_room != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() {
updateReplyModel();
resetModel();
});
initializeEvent();
updateReplyModel();
resetModel();
}
void MessageContentModel::initializeEvent()
{
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr) {
Q_EMIT eventUnavailable();
return;
}
if (m_eventSenderObject == nullptr) {
auto senderId = event->senderId();
// A pending event might not have a sender ID set yet but in that case it must
// be the local member.
if (senderId.isEmpty()) {
senderId = m_room->localMember().id();
}
m_eventSenderObject = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, senderId));
}
Q_EMIT eventUpdated();
}
void MessageContentModel::getEvent()
{
Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) { Quotient::connectUntil(m_room.get(), &NeoChatRoom::extraEventLoaded, this, [this](const QString &eventId) {
if (m_room != nullptr) { if (m_room != nullptr) {
if (eventId == m_eventId) { if (eventId == m_eventId) {
m_notFound = false; m_notFound = false;
intiializeEvent(m_room->getEvent(eventId)); initializeEvent();
updateReplyModel(); updateReplyModel();
resetModel(); resetModel();
return true; return true;
@@ -83,130 +191,7 @@ void MessageContentModel::initializeModel()
return false; return false;
}); });
if (m_event == nullptr) { m_room->downloadEventFromServer(m_eventId);
intiializeEvent(m_room->getEvent(m_eventId));
if (m_event == nullptr) {
m_room->downloadEventFromServer(m_eventId);
}
}
connect(m_room, &NeoChatRoom::pendingEventAboutToMerge, this, [this](Quotient::RoomEvent *serverEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventId == serverEvent->id() || m_eventId == serverEvent->transactionId()) {
beginResetModel();
m_isPending = false;
intiializeEvent(serverEvent);
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::replacedEvent, this, [this](const Quotient::RoomEvent *newEvent) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventId == newEvent->id()) {
beginResetModel();
intiializeEvent(newEvent);
resetContent();
endResetModel();
}
}
});
connect(m_room, &NeoChatRoom::newFileTransfer, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferProgress, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_eventId) {
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferCompleted, this, [this](const QString &eventId) {
if (m_room != nullptr && m_event != nullptr && eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room, &NeoChatRoom::fileTransferFailed, this, [this](const QString &eventId) {
if (m_event != nullptr && eventId == m_eventId) {
resetContent();
Q_EMIT dataChanged(index(0), index(rowCount() - 1), {FileTransferInfoRole});
}
});
connect(m_room->editCache(), &ChatBarCache::relationIdChanged, this, [this](const QString &oldEventId, const QString &newEventId) {
if (m_event != nullptr && (oldEventId == m_eventId || newEventId == m_eventId)) {
// HACK: Because DelegateChooser can't switch the delegate on dataChanged it has to think there is a new delegate.
beginResetModel();
resetContent(newEventId == m_eventId);
endResetModel();
}
});
connect(m_room->threadCache(), &ChatBarCache::threadIdChanged, this, [this](const QString &oldThreadId, const QString &newThreadId) {
if (m_event != nullptr && (oldThreadId == m_eventId || newThreadId == m_eventId)) {
beginResetModel();
resetContent(false, newThreadId == m_eventId);
endResetModel();
}
});
connect(m_room, &NeoChatRoom::urlPreviewEnabledChanged, this, [this]() {
resetContent();
});
connect(NeoChatConfig::self(), &NeoChatConfig::ShowLinkPreviewChanged, this, [this]() {
resetContent();
});
connect(m_room, &Room::memberNameUpdated, this, [this](RoomMember member) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(m_room, &Room::memberAvatarUpdated, this, [this](RoomMember member) {
if (m_room != nullptr && m_event != nullptr) {
if (m_eventSenderId.isEmpty() || m_eventSenderId == member.id()) {
Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole});
}
}
});
connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() {
updateReplyModel();
resetModel();
});
if (m_event != nullptr) {
updateReplyModel();
}
resetModel();
}
void MessageContentModel::intiializeEvent(const QString &eventId)
{
const auto newEvent = m_room->getEvent(eventId);
if (newEvent != nullptr) {
intiializeEvent(newEvent);
}
}
void MessageContentModel::intiializeEvent(const Quotient::RoomEvent *event)
{
if (event == nullptr) {
return;
}
m_event = loadEvent<RoomEvent>(event->fullJson());
// a pending event may not previously have had an event ID so update.
m_eventId = EventHandler::id(m_event.get());
auto senderId = m_event->senderId();
// A pending event might not have a sender ID set yet but in that case it must
// be the local member.
if (senderId.isEmpty()) {
senderId = m_room->localMember().id();
}
if (m_eventSenderObject == nullptr) {
m_eventSenderObject = std::unique_ptr<NeochatRoomMember>(new NeochatRoomMember(m_room, senderId));
}
Q_EMIT eventUpdated();
} }
bool MessageContentModel::showAuthor() const bool MessageContentModel::showAuthor() const
@@ -221,7 +206,7 @@ void MessageContentModel::setShowAuthor(bool showAuthor)
} }
m_showAuthor = showAuthor; m_showAuthor = showAuthor;
if (m_event != nullptr && m_room->connection()->isIgnored(m_event->senderId())) { if (m_room->connection()->isIgnored(m_eventSenderId)) {
if (showAuthor) { if (showAuthor) {
beginInsertRows({}, 0, 0); beginInsertRows({}, 0, 0);
m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}}); m_components.prepend(MessageComponent{MessageComponentType::Author, QString(), {}});
@@ -250,8 +235,23 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
const auto component = m_components[index.row()]; const auto component = m_components[index.row()];
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr) {
if (role == DisplayRole) {
if (m_isReply) {
return i18n("Loading reply");
} else {
return i18n("Loading");
}
}
if (role == ComponentTypeRole) {
return component.type;
}
return {};
}
if (role == DisplayRole) { if (role == DisplayRole) {
if (m_notFound || (m_event && m_room->connection()->isIgnored(m_event->senderId()))) { if (m_notFound || m_room->connection()->isIgnored(m_eventSenderId)) {
Kirigami::Platform::PlatformTheme *theme = Kirigami::Platform::PlatformTheme *theme =
static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true)); static_cast<Kirigami::Platform::PlatformTheme *>(qmlAttachedPropertiesObject<Kirigami::Platform::PlatformTheme>(this, true));
@@ -265,16 +265,17 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
+ i18nc("@info", "This message was either not found, you do not have permission to view it, or it was sent by an ignored user") + i18nc("@info", "This message was either not found, you do not have permission to view it, or it was sent by an ignored user")
+ QStringLiteral("</span>")); + QStringLiteral("</span>"));
} }
if (component.type == MessageComponentType::Loading && m_isReply) { if (component.type == MessageComponentType::Loading) {
return i18n("Loading reply"); if (m_isReply) {
} return i18n("Loading reply");
if (m_event == nullptr) { } else {
return QString(); return i18n("Loading");
}
} }
if (!component.content.isEmpty()) { if (!component.content.isEmpty()) {
return component.content; return component.content;
} }
return EventHandler::richBody(m_room, m_event.get()); return EventHandler::richBody(m_room, event);
} }
if (role == ComponentTypeRole) { if (role == ComponentTypeRole) {
return component.type; return component.type;
@@ -283,53 +284,53 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const
return component.attributes; return component.attributes;
} }
if (role == EventIdRole) { if (role == EventIdRole) {
return EventHandler::id(m_event.get()); return EventHandler::id(event);
} }
if (role == TimeRole) { if (role == TimeRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) { const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
return m_event->transactionId() == pendingEvent->transactionId(); return event->transactionId() == pendingEvent->transactionId();
}); });
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated(); auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return EventHandler::time(m_event.get(), m_isPending, lastUpdated); return EventHandler::time(event, m_isPending, lastUpdated);
} }
if (role == TimeStringRole) { if (role == TimeStringRole) {
const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [this](const PendingEventItem &pendingEvent) { const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) {
return m_event->transactionId() == pendingEvent->transactionId(); return event->transactionId() == pendingEvent->transactionId();
}); });
auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated(); auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated();
return EventHandler::timeString(m_event.get(), QStringLiteral("hh:mm"), m_isPending, lastUpdated); return EventHandler::timeString(event, QStringLiteral("hh:mm"), m_isPending, lastUpdated);
} }
if (role == AuthorRole) { if (role == AuthorRole) {
return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get()); return QVariant::fromValue<NeochatRoomMember *>(m_eventSenderObject.get());
} }
if (role == MediaInfoRole) { if (role == MediaInfoRole) {
return EventHandler::mediaInfo(m_room, m_event.get()); return EventHandler::mediaInfo(m_room, event);
} }
if (role == FileTransferInfoRole) { if (role == FileTransferInfoRole) {
return QVariant::fromValue(m_room->cachedFileTransferInfo(m_event.get())); return QVariant::fromValue(m_room->cachedFileTransferInfo(event));
} }
if (role == ItineraryModelRole) { if (role == ItineraryModelRole) {
return QVariant::fromValue<ItineraryModel *>(m_itineraryModel); return QVariant::fromValue<ItineraryModel *>(m_itineraryModel);
} }
if (role == LatitudeRole) { if (role == LatitudeRole) {
return EventHandler::latitude(m_event.get()); return EventHandler::latitude(event);
} }
if (role == LongitudeRole) { if (role == LongitudeRole) {
return EventHandler::longitude(m_event.get()); return EventHandler::longitude(event);
} }
if (role == AssetRole) { if (role == AssetRole) {
return EventHandler::locationAssetType(m_event.get()); return EventHandler::locationAssetType(event);
} }
if (role == PollHandlerRole) { if (role == PollHandlerRole) {
return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId)); return QVariant::fromValue<PollHandler *>(m_room->poll(m_eventId));
} }
if (role == ReplyEventIdRole) { if (role == ReplyEventIdRole) {
return EventHandler::replyId(m_event.get()); return EventHandler::replyId(event);
} }
if (role == ReplyAuthorRole) { if (role == ReplyAuthorRole) {
return QVariant::fromValue(EventHandler::replyAuthor(m_room, m_event.get())); return QVariant::fromValue(EventHandler::replyAuthor(m_room, event));
} }
if (role == ReplyContentModelRole) { if (role == ReplyContentModelRole) {
return QVariant::fromValue<MessageContentModel *>(m_replyModel); return QVariant::fromValue<MessageContentModel *>(m_replyModel);
@@ -385,16 +386,18 @@ QHash<int, QByteArray> MessageContentModel::roleNames() const
void MessageContentModel::resetModel() void MessageContentModel::resetModel()
{ {
const auto event = m_room->getEvent(m_eventId);
beginResetModel(); beginResetModel();
m_components.clear(); m_components.clear();
if ((m_event && m_room->connection()->isIgnored(m_event->senderId())) || m_notFound) { if (m_room->connection()->isIgnored(m_eventSenderId) || m_notFound) {
m_components += MessageComponent{MessageComponentType::Text, QString(), {}}; m_components += MessageComponent{MessageComponentType::Text, QString(), {}};
endResetModel(); endResetModel();
return; return;
} }
if (m_event == nullptr) { if (event == nullptr) {
m_components += MessageComponent{MessageComponentType::Loading, QString(), {}}; m_components += MessageComponent{MessageComponentType::Loading, QString(), {}};
endResetModel(); endResetModel();
return; return;
@@ -410,8 +413,6 @@ void MessageContentModel::resetModel()
void MessageContentModel::resetContent(bool isEditing, bool isThreading) void MessageContentModel::resetContent(bool isEditing, bool isThreading)
{ {
Q_ASSERT(m_event != nullptr);
const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0; const auto startRow = m_components[0].type == MessageComponentType::Author ? 1 : 0;
beginRemoveRows({}, startRow, rowCount() - 1); beginRemoveRows({}, startRow, rowCount() - 1);
m_components.remove(startRow, rowCount() - startRow); m_components.remove(startRow, rowCount() - startRow);
@@ -428,15 +429,20 @@ void MessageContentModel::resetContent(bool isEditing, bool isThreading)
QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading) QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEditing, bool isThreading)
{ {
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr) {
return {};
}
QList<MessageComponent> newComponents; QList<MessageComponent> newComponents;
if (eventCast<const Quotient::RoomMessageEvent>(m_event) if (eventCast<const Quotient::RoomMessageEvent>(event)
&& eventCast<const Quotient::RoomMessageEvent>(m_event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) { && eventCast<const Quotient::RoomMessageEvent>(event)->rawMsgtype() == QStringLiteral("m.key.verification.request")) {
newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}}; newComponents += MessageComponent{MessageComponentType::Verification, QString(), {}};
return newComponents; return newComponents;
} }
if (m_event->isRedacted()) { if (event->isRedacted()) {
newComponents += MessageComponent{MessageComponentType::Text, QString(), {}}; newComponents += MessageComponent{MessageComponentType::Text, QString(), {}};
return newComponents; return newComponents;
} }
@@ -448,7 +454,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
if (isEditing) { if (isEditing) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
} else { } else {
newComponents.append(componentsForType(MessageComponentType::typeForEvent(*m_event.get()))); newComponents.append(componentsForType(MessageComponentType::typeForEvent(*event)));
} }
if (m_room->urlPreviewEnabled()) { if (m_room->urlPreviewEnabled()) {
@@ -456,7 +462,7 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
} }
// If the event is already threaded the ThreadModel will handle displaying a chat bar. // If the event is already threaded the ThreadModel will handle displaying a chat bar.
if (isThreading && !EventHandler::isThreaded(m_event.get())) { if (isThreading && !EventHandler::isThreaded(event)) {
newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}}; newComponents += MessageComponent{MessageComponentType::ChatBar, QString(), {}};
} }
@@ -465,11 +471,12 @@ QList<MessageComponent> MessageContentModel::messageContentComponents(bool isEdi
void MessageContentModel::updateReplyModel() void MessageContentModel::updateReplyModel()
{ {
if (m_event == nullptr || m_isReply) { const auto event = m_room->getEvent(m_eventId);
if (event == nullptr || m_isReply) {
return; return;
} }
if (!EventHandler::hasReply(m_event.get()) || (EventHandler::isThreaded(m_event.get()) && NeoChatConfig::self()->threads())) { if (!EventHandler::hasReply(event) || (EventHandler::isThreaded(event) && NeoChatConfig::self()->threads())) {
if (m_replyModel) { if (m_replyModel) {
delete m_replyModel; delete m_replyModel;
} }
@@ -480,12 +487,7 @@ void MessageContentModel::updateReplyModel()
return; return;
} }
const auto replyEvent = m_room->findInTimeline(EventHandler::replyId(m_event.get())); m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(event), true, false, this);
if (replyEvent == m_room->historyEdge()) {
m_replyModel = new MessageContentModel(m_room, EventHandler::replyId(m_event.get()), true, false, this);
} else {
m_replyModel = new MessageContentModel(m_room, replyEvent->get(), true, false, this);
}
connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() { connect(m_replyModel, &MessageContentModel::eventUpdated, this, [this]() {
Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole}); Q_EMIT dataChanged(index(0), index(0), {ReplyAuthorRole});
@@ -494,28 +496,37 @@ void MessageContentModel::updateReplyModel()
QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type) QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentType::Type type)
{ {
const auto event = m_room->getEvent(m_eventId);
if (event == nullptr) {
return {};
}
switch (type) { switch (type) {
case MessageComponentType::Text: { case MessageComponentType::Text: {
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event); const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
auto body = EventHandler::rawMessageBody(*event); auto body = EventHandler::rawMessageBody(*roomMessageEvent);
return TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced()); return TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
} }
case MessageComponentType::File: { case MessageComponentType::File: {
QList<MessageComponent> components; QList<MessageComponent> components;
components += MessageComponent{MessageComponentType::File, QString(), {}}; components += MessageComponent{MessageComponentType::File, QString(), {}};
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event); const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
if (m_emptyItinerary) { if (m_emptyItinerary) {
if (!m_isReply) { if (!m_isReply) {
auto fileTransferInfo = m_room->cachedFileTransferInfo(m_event.get()); auto fileTransferInfo = m_room->cachedFileTransferInfo(event);
#ifndef Q_OS_ANDROID #ifndef Q_OS_ANDROID
Q_ASSERT(event->content() != nullptr && event->content()->fileInfo() != nullptr); Q_ASSERT(roomMessageEvent->content() != nullptr && roomMessageEvent->content()->fileInfo() != nullptr);
const QMimeType mimeType = event->content()->fileInfo()->mimeType; const QMimeType mimeType = roomMessageEvent->content()->fileInfo()->mimeType;
if (mimeType.name() == QStringLiteral("text/plain") || mimeType.parentMimeTypes().contains(QStringLiteral("text/plain"))) { if (mimeType.name() == QStringLiteral("text/plain") || mimeType.parentMimeTypes().contains(QStringLiteral("text/plain"))) {
QString originalName = event->content()->fileInfo()->originalName; QString originalName = roomMessageEvent->content()->fileInfo()->originalName;
if (originalName.isEmpty()) { if (originalName.isEmpty()) {
originalName = event->plainBody(); originalName = roomMessageEvent->plainBody();
} }
KSyntaxHighlighting::Repository repository; KSyntaxHighlighting::Repository repository;
KSyntaxHighlighting::Definition definitionForFile = repository.definitionForFileName(originalName); KSyntaxHighlighting::Definition definitionForFile = repository.definitionForFileName(originalName);
@@ -544,19 +555,27 @@ QList<MessageComponent> MessageContentModel::componentsForType(MessageComponentT
} else { } else {
updateItineraryModel(); updateItineraryModel();
} }
auto body = EventHandler::rawMessageBody(*event); auto body = EventHandler::rawMessageBody(*roomMessageEvent);
components += TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced()); components += TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
return components; return components;
} }
case MessageComponentType::Image: case MessageComponentType::Image:
case MessageComponentType::Audio: case MessageComponentType::Audio:
case MessageComponentType::Video: { case MessageComponentType::Video: {
if (!m_event->is<StickerEvent>()) { if (!event->is<StickerEvent>()) {
const auto event = eventCast<const Quotient::RoomMessageEvent>(m_event); const auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event);
QList<MessageComponent> components; QList<MessageComponent> components;
components += MessageComponent{type, QString(), {}}; components += MessageComponent{type, QString(), {}};
auto body = EventHandler::rawMessageBody(*event); auto body = EventHandler::rawMessageBody(*roomMessageEvent);
components += TextHandler().textComponents(body, EventHandler::messageBodyInputFormat(*event), m_room, event, event->isReplaced()); components += TextHandler().textComponents(body,
EventHandler::messageBodyInputFormat(*roomMessageEvent),
m_room,
roomMessageEvent,
roomMessageEvent->isReplaced());
return components; return components;
} }
} }
@@ -626,13 +645,14 @@ void MessageContentModel::closeLinkPreview(int row)
void MessageContentModel::updateItineraryModel() void MessageContentModel::updateItineraryModel()
{ {
if (m_room == nullptr || m_event == nullptr) { const auto event = m_room->getEvent(m_eventId);
if (m_room == nullptr || event == nullptr) {
return; return;
} }
if (auto event = eventCast<const Quotient::RoomMessageEvent>(m_event)) { if (auto roomMessageEvent = eventCast<const Quotient::RoomMessageEvent>(event)) {
if (event->hasFileContent()) { if (roomMessageEvent->hasFileContent()) {
auto filePath = m_room->cachedFileTransferInfo(m_event.get()).localPath; auto filePath = m_room->cachedFileTransferInfo(event).localPath;
if (filePath.isEmpty() && m_itineraryModel != nullptr) { if (filePath.isEmpty() && m_itineraryModel != nullptr) {
delete m_itineraryModel; delete m_itineraryModel;
m_itineraryModel = nullptr; m_itineraryModel = nullptr;

View File

@@ -75,11 +75,10 @@ public:
Q_ENUM(Roles) Q_ENUM(Roles)
explicit MessageContentModel(NeoChatRoom *room, explicit MessageContentModel(NeoChatRoom *room,
const Quotient::RoomEvent *event, const QString &eventId,
bool isReply = false, bool isReply = false,
bool isPending = false, bool isPending = false,
MessageContentModel *parent = nullptr); MessageContentModel *parent = nullptr);
MessageContentModel(NeoChatRoom *room, const QString &eventId, bool isReply = false, bool isPending = false, MessageContentModel *parent = nullptr);
bool showAuthor() const; bool showAuthor() const;
void setShowAuthor(bool showAuthor); void setShowAuthor(bool showAuthor);
@@ -114,6 +113,7 @@ public:
Q_SIGNALS: Q_SIGNALS:
void showAuthorChanged(); void showAuthorChanged();
void eventUnavailable();
void eventUpdated(); void eventUpdated();
private: private:
@@ -121,7 +121,6 @@ private:
QString m_eventId; QString m_eventId;
QString m_eventSenderId; QString m_eventSenderId;
std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr; std::unique_ptr<NeochatRoomMember> m_eventSenderObject = nullptr;
Quotient::RoomEventPtr m_event;
bool m_isPending; bool m_isPending;
bool m_showAuthor = true; bool m_showAuthor = true;
@@ -129,8 +128,8 @@ private:
bool m_notFound = false; bool m_notFound = false;
void initializeModel(); void initializeModel();
void intiializeEvent(const QString &eventId); void initializeEvent();
void intiializeEvent(const Quotient::RoomEvent *event); void getEvent();
QList<MessageComponent> m_components; QList<MessageComponent> m_components;
void resetModel(); void resetModel();

View File

@@ -639,7 +639,7 @@ void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event)
if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) { if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) {
if (!event->isStateEvent() || event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) { if (!event->isStateEvent() || event->matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_currentRoom, event)); m_contentModels[eventId] = std::unique_ptr<MessageContentModel>(new MessageContentModel(m_currentRoom, eventId));
} }
} }

View File

@@ -115,11 +115,11 @@ QVariant SearchModel::data(const QModelIndex &index, int role) const
return EventHandler::threadRoot(&event); return EventHandler::threadRoot(&event);
case ContentModelRole: { case ContentModelRole: {
if (!event.isStateEvent()) { if (!event.isStateEvent()) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, &event)); return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, event.id()));
} }
if (event.isStateEvent()) { if (event.isStateEvent()) {
if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) { if (event.matrixType() == QStringLiteral("org.matrix.msc3672.beacon_info")) {
return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, &event)); return QVariant::fromValue<MessageContentModel *>(new MessageContentModel(m_room, event.id()));
} }
} }
return {}; return {};

View File

@@ -80,7 +80,7 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent()); const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
auto newEvents = m_currentJob->chunk(); auto newEvents = m_currentJob->chunk();
for (auto &event : newEvents) { for (auto &event : newEvents) {
m_contentModels.push_back(new MessageContentModel(room, event.get())); m_contentModels.push_back(new MessageContentModel(room, event->id()));
} }
addModels(); addModels();
@@ -103,7 +103,7 @@ void ThreadModel::fetchMore(const QModelIndex &parent)
void ThreadModel::addNewEvent(const Quotient::RoomEvent *event) void ThreadModel::addNewEvent(const Quotient::RoomEvent *event)
{ {
const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent()); const auto room = dynamic_cast<NeoChatRoom *>(QObject::parent());
m_contentModels.push_front(new MessageContentModel(room, event)); m_contentModels.push_front(new MessageContentModel(room, event->id()));
} }
void ThreadModel::addModels() void ThreadModel::addModels()

View File

@@ -1768,6 +1768,9 @@ QByteArray NeoChatRoom::roomAcountDataJson(const QString &eventType)
void NeoChatRoom::downloadEventFromServer(const QString &eventId) void NeoChatRoom::downloadEventFromServer(const QString &eventId)
{ {
if (findInTimeline(eventId) != historyEdge()) { if (findInTimeline(eventId) != historyEdge()) {
// For whatever reason the event has now appeared so the function that called
// this need to whatever it wanted to do with the event.
Q_EMIT extraEventLoaded(eventId);
return; return;
} }
auto job = connection()->callApi<GetOneRoomEventJob>(id(), eventId); auto job = connection()->callApi<GetOneRoomEventJob>(id(), eventId);