diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 16c350392..a374a7a15 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -50,9 +50,9 @@ ecm_add_test( ) ecm_add_test( - messageeventmodeltest.cpp + timelinemessagemodeltest.cpp LINK_LIBRARIES neochat Qt::Test - TEST_NAME messageeventmodeltest + TEST_NAME timelinemessagemodeltest ) ecm_add_test( diff --git a/autotests/eventhandlertest.cpp b/autotests/eventhandlertest.cpp index 9f41af814..a99a9a8f4 100644 --- a/autotests/eventhandlertest.cpp +++ b/autotests/eventhandlertest.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include "linkpreviewer.h" #include "models/reactionmodel.h" @@ -98,18 +100,24 @@ void EventHandlerTest::time() { const auto event = room->messageEvents().at(0).get(); - QCOMPARE(EventHandler::time(event), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC))); - QCOMPARE(EventHandler::time(event, true, QDateTime::fromMSecsSinceEpoch(1234, QTimeZone(QTimeZone::UTC))), - QDateTime::fromMSecsSinceEpoch(1234, QTimeZone(QTimeZone::UTC))); + QCOMPARE(EventHandler::time(room, event), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC))); + + const auto txID = room->postJson("m.room.message"_L1, event->fullJson()); + QCOMPARE(room->pendingEvents().size(), 1); + const auto pendingIt = room->findPendingEvent(txID); + QCOMPARE(EventHandler::time(room, pendingIt->event(), true), pendingIt->lastUpdated()); + + room->discardMessage(txID); + QCOMPARE(room->pendingEvents().size(), 0); } void EventHandlerTest::nullTime() { - QTest::ignoreMessage(QtWarningMsg, "time called with event set to nullptr."); - QCOMPARE(EventHandler::time(nullptr), QDateTime()); + QTest::ignoreMessage(QtWarningMsg, "time called with room set to nullptr."); + QCOMPARE(EventHandler::time(nullptr, nullptr), QDateTime()); - QTest::ignoreMessage(QtWarningMsg, "a value must be provided for lastUpdated for a pending event."); - QCOMPARE(EventHandler::time(room->messageEvents().at(0).get(), true), QDateTime()); + QTest::ignoreMessage(QtWarningMsg, "time called with event set to nullptr."); + QCOMPARE(EventHandler::time(room, nullptr), QDateTime()); } void EventHandlerTest::timeString() @@ -118,19 +126,27 @@ void EventHandlerTest::timeString() KFormat format; - QCOMPARE(EventHandler::timeString(event, false), + QCOMPARE(EventHandler::timeString(room, event, false), QLocale().toString(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat)); - QCOMPARE(EventHandler::timeString(event, true), + QCOMPARE(EventHandler::timeString(room, event, true), format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::ShortFormat)); - QCOMPARE(EventHandler::timeString(event, false, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC))), - QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::ShortFormat)); - QCOMPARE(EventHandler::timeString(event, true, QLocale::ShortFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC))), - format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::ShortFormat)); - QCOMPARE(EventHandler::timeString(event, false, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC))), - QLocale().toString(QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC)).toLocalTime().time(), QLocale::LongFormat)); - QCOMPARE(EventHandler::timeString(event, true, QLocale::LongFormat, true, QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC))), - format.formatRelativeDate(QDateTime::fromMSecsSinceEpoch(1690699214545, QTimeZone(QTimeZone::UTC)).toLocalTime().date(), QLocale::LongFormat)); - QCOMPARE(EventHandler::timeString(event, u"hh:mm"_s), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toString(u"hh:mm"_s)); + QCOMPARE(EventHandler::timeString(room, event, u"hh:mm"_s), QDateTime::fromMSecsSinceEpoch(1432735824654, QTimeZone(QTimeZone::UTC)).toString(u"hh:mm"_s)); + + const auto txID = room->postJson("m.room.message"_L1, event->fullJson()); + QCOMPARE(room->pendingEvents().size(), 1); + const auto pendingIt = room->findPendingEvent(txID); + + QCOMPARE(EventHandler::timeString(room, pendingIt->event(), false, QLocale::ShortFormat, true), + QLocale().toString(pendingIt->lastUpdated().toLocalTime().time(), QLocale::ShortFormat)); + QCOMPARE(EventHandler::timeString(room, pendingIt->event(), true, QLocale::ShortFormat, true), + format.formatRelativeDate(pendingIt->lastUpdated().toLocalTime().date(), QLocale::ShortFormat)); + QCOMPARE(EventHandler::timeString(room, pendingIt->event(), false, QLocale::LongFormat, true), + QLocale().toString(pendingIt->lastUpdated().toLocalTime().time(), QLocale::LongFormat)); + QCOMPARE(EventHandler::timeString(room, pendingIt->event(), true, QLocale::LongFormat, true), + format.formatRelativeDate(pendingIt->lastUpdated().toLocalTime().date(), QLocale::LongFormat)); + + room->discardMessage(txID); + QCOMPARE(room->pendingEvents().size(), 0); } void EventHandlerTest::highlighted() diff --git a/autotests/messageeventmodeltest.cpp b/autotests/timelinemessagemodeltest.cpp similarity index 84% rename from autotests/messageeventmodeltest.cpp rename to autotests/timelinemessagemodeltest.cpp index c4ce44797..bc1f037fb 100644 --- a/autotests/messageeventmodeltest.cpp +++ b/autotests/timelinemessagemodeltest.cpp @@ -10,20 +10,20 @@ #include #include "enums/delegatetype.h" -#include "models/messageeventmodel.h" +#include "models/timelinemessagemodel.h" #include "neochatroom.h" #include "testutils.h" using namespace Quotient; -class MessageEventModelTest : public QObject +class TimelineMessageModelTest : public QObject { Q_OBJECT private: Connection *connection = nullptr; - MessageEventModel *model = nullptr; + TimelineMessageModel *model = nullptr; private Q_SLOTS: void initTestCase(); @@ -40,19 +40,19 @@ private Q_SLOTS: void cleanup(); }; -void MessageEventModelTest::initTestCase() +void TimelineMessageModelTest::initTestCase() { connection = Connection::makeMockConnection(u"@bob:kde.org"_s); } -void MessageEventModelTest::init() +void TimelineMessageModelTest::init() { QCOMPARE(model, nullptr); - model = new MessageEventModel; + model = new TimelineMessageModel; } // Make sure that basic empty rooms can be switched without crashing. -void MessageEventModelTest::switchEmptyRoom() +void TimelineMessageModelTest::switchEmptyRoom() { auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s); auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s); @@ -72,7 +72,7 @@ void MessageEventModelTest::switchEmptyRoom() } // Make sure that rooms with some events can be switched without crashing -void MessageEventModelTest::switchSyncedRoom() +void TimelineMessageModelTest::switchSyncedRoom() { auto firstRoom = new TestUtils::TestRoom(connection, u"#firstRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s); auto secondRoom = new TestUtils::TestRoom(connection, u"#secondRoom:kde.org"_s, u"test-messageventmodel-sync.json"_s); @@ -91,19 +91,19 @@ void MessageEventModelTest::switchSyncedRoom() QCOMPARE(model->room(), nullptr); } -void MessageEventModelTest::simpleTimeline() +void TimelineMessageModelTest::simpleTimeline() { auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-messageventmodel-sync.json"_s); model->setRoom(room); QCOMPARE(model->rowCount(), 2); - QCOMPARE(model->data(model->index(0), MessageEventModel::DelegateTypeRole), DelegateType::State); + QCOMPARE(model->data(model->index(0), TimelineMessageModel::DelegateTypeRole), DelegateType::State); QCOMPARE(model->data(model->index(0)), u"changed their display name to Example Changed"_s); QCOMPARE(model->data(model->index(1)), u"This is an example
text message
"_s); - QCOMPARE(model->data(model->index(1), MessageEventModel::DelegateTypeRole), DelegateType::Message); - QCOMPARE(model->data(model->index(1), MessageEventModel::EventIdRole), u"$153456789:example.org"_s); + QCOMPARE(model->data(model->index(1), TimelineMessageModel::DelegateTypeRole), DelegateType::Message); + QCOMPARE(model->data(model->index(1), TimelineMessageModel::EventIdRole), u"$153456789:example.org"_s); QTest::ignoreMessage(QtWarningMsg, "Index QModelIndex(-1,-1,0x0,QObject(0x0)) is not valid (expected valid)"); QCOMPARE(model->data(model->index(-1)), QVariant()); @@ -111,8 +111,8 @@ void MessageEventModelTest::simpleTimeline() QCOMPARE(model->data(model->index(model->rowCount())), QVariant()); } -// Sync some events into the MessageEventModel's current room and don't crash. -void MessageEventModelTest::syncNewEvents() +// Sync some events into the TimelineMessageModel's current room and don't crash. +void TimelineMessageModelTest::syncNewEvents() { auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s); QSignalSpy spy(room, SIGNAL(aboutToAddNewMessages(Quotient::RoomEventsRange))); @@ -127,7 +127,7 @@ void MessageEventModelTest::syncNewEvents() } // Check the adding of pending events to the room doesn't cause any issues in the model. -void MessageEventModelTest::pendingEvent() +void TimelineMessageModelTest::pendingEvent() { QSignalSpy spyInsert(model, SIGNAL(rowsInserted(const QModelIndex &, int, int))); QSignalSpy spyRemove(model, SIGNAL(rowsRemoved(const QModelIndex &, int, int))); @@ -174,7 +174,7 @@ void MessageEventModelTest::pendingEvent() auto isPendingChanged = false; for (auto signal : spyChanged) { auto roles = signal.at(2).toList(); - if (roles.contains(MessageEventModel::IsPendingRole)) { + if (roles.contains(TimelineMessageModel::IsPendingRole)) { isPendingChanged = true; } } @@ -182,7 +182,7 @@ void MessageEventModelTest::pendingEvent() } // Make sure that the signals are disconnecting correctly when a room is switched. -void MessageEventModelTest::disconnect() +void TimelineMessageModelTest::disconnect() { auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s); model->setRoom(room); @@ -195,7 +195,7 @@ void MessageEventModelTest::disconnect() QCOMPARE(spy.count(), 0); } -void MessageEventModelTest::idToRow() +void TimelineMessageModelTest::idToRow() { auto room = new TestUtils::TestRoom(connection, u"#myroom:kde.org"_s, u"test-min-sync.json"_s); model->setRoom(room); @@ -203,12 +203,12 @@ void MessageEventModelTest::idToRow() QCOMPARE(model->eventIdToRow(u"$153456789:example.org"_s), 0); } -void MessageEventModelTest::cleanup() +void TimelineMessageModelTest::cleanup() { delete model; model = nullptr; QCOMPARE(model, nullptr); } -QTEST_MAIN(MessageEventModelTest) -#include "messageeventmodeltest.moc" +QTEST_MAIN(TimelineMessageModelTest) +#include "timelinemessagemodeltest.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 53b2d7acf..07c9b0a33 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,8 +18,8 @@ add_library(neochat STATIC models/customemojimodel.h clipboard.cpp clipboard.h - models/messageeventmodel.cpp - models/messageeventmodel.h + models/timelinemessagemodel.cpp + models/timelinemessagemodel.h models/messagefiltermodel.cpp models/messagefiltermodel.h models/roomlistmodel.cpp @@ -192,6 +192,8 @@ add_library(neochat STATIC enums/roomsortparameter.h models/roomsortparametermodel.cpp models/roomsortparametermodel.h + models/messagemodel.cpp + models/messagemodel.h ) set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES @@ -325,10 +327,10 @@ if(WIN32) endif() ecm_qt_declare_logging_category(neochat - HEADER "messageeventmodel_logging.h" - IDENTIFIER "MessageEvent" - CATEGORY_NAME "org.kde.neochat.messageeventmodel" - DESCRIPTION "Neochat: messageeventmodel" + HEADER "messagemodel_logging.h" + IDENTIFIER "Message" + CATEGORY_NAME "org.kde.neochat.messagemodel" + DESCRIPTION "Neochat: messagemodel" DEFAULT_SEVERITY Info EXPORT NEOCHAT ) diff --git a/src/eventhandler.cpp b/src/eventhandler.cpp index 938706c9c..3817acd88 100644 --- a/src/eventhandler.cpp +++ b/src/eventhandler.cpp @@ -95,23 +95,30 @@ QString EventHandler::singleLineAuthorDisplayname(const NeoChatRoom *room, const return displayName; } -QDateTime EventHandler::time(const Quotient::RoomEvent *event, bool isPending, QDateTime lastUpdated) +QDateTime EventHandler::time(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending) { + if (room == nullptr) { + qCWarning(EventHandling) << "time called with room set to nullptr."; + return {}; + } if (event == nullptr) { qCWarning(EventHandling) << "time called with event set to nullptr."; return {}; } - if (isPending && lastUpdated == QDateTime()) { - qCWarning(EventHandling) << "a value must be provided for lastUpdated for a pending event."; + + if (isPending) { + const auto pendingIt = room->findPendingEvent(event->transactionId()); + if (pendingIt != room->pendingEvents().end()) { + return pendingIt->lastUpdated(); + } return {}; } - - return isPending ? lastUpdated : event->originTimestamp(); + return event->originTimestamp(); } -QString EventHandler::timeString(const Quotient::RoomEvent *event, bool relative, QLocale::FormatType format, bool isPending, QDateTime lastUpdated) +QString EventHandler::timeString(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool relative, QLocale::FormatType format, bool isPending) { - auto ts = time(event, isPending, lastUpdated); + auto ts = time(room, event, isPending); if (ts.isValid()) { if (relative) { KFormat formatter; @@ -123,9 +130,9 @@ QString EventHandler::timeString(const Quotient::RoomEvent *event, bool relative return {}; } -QString EventHandler::timeString(const Quotient::RoomEvent *event, const QString &format, bool isPending, const QDateTime &lastUpdated) +QString EventHandler::timeString(const NeoChatRoom *room, const Quotient::RoomEvent *event, const QString &format, bool isPending) { - return time(event, isPending, lastUpdated).toLocalTime().toString(format); + return time(room, event, isPending).toLocalTime().toString(format); } bool EventHandler::isHighlighted(const NeoChatRoom *room, const Quotient::RoomEvent *event) diff --git a/src/eventhandler.h b/src/eventhandler.h index 1220d43c9..3c71e463b 100644 --- a/src/eventhandler.h +++ b/src/eventhandler.h @@ -64,7 +64,7 @@ public: /** * @brief Return a QDateTime object for the event timestamp. */ - static QDateTime time(const Quotient::RoomEvent *event, bool isPending = false, QDateTime lastUpdated = {}); + static QDateTime time(const NeoChatRoom *room, const Quotient::RoomEvent *event, bool isPending = false); /** * @brief Return a QString for the event timestamp. @@ -80,11 +80,11 @@ public: * @param lastUpdated the time the event was last updated locally as this cannot be * obtained from the event. */ - static QString timeString(const Quotient::RoomEvent *event, + static QString timeString(const NeoChatRoom *room, + const Quotient::RoomEvent *event, bool relative, QLocale::FormatType format = QLocale::ShortFormat, - bool isPending = false, - QDateTime lastUpdated = {}); + bool isPending = false); /** * @brief Return a QString for the event timestamp. @@ -98,7 +98,7 @@ public: * @param lastUpdated the time the event was last updated locally as this cannot be * obtained from the event. */ - static QString timeString(const Quotient::RoomEvent *event, const QString &format, bool isPending = false, const QDateTime &lastUpdated = {}); + static QString timeString(const NeoChatRoom *room, const Quotient::RoomEvent *event, const QString &format, bool isPending = false); /** * @brief Whether the event should be highlighted in the timeline. diff --git a/src/models/mediamessagefiltermodel.cpp b/src/models/mediamessagefiltermodel.cpp index 19bf422f3..2caf8b9f6 100644 --- a/src/models/mediamessagefiltermodel.cpp +++ b/src/models/mediamessagefiltermodel.cpp @@ -7,8 +7,8 @@ #include #include "messagecontentmodel.h" -#include "messageeventmodel.h" #include "messagefiltermodel.h" +#include "timelinemessagemodel.h" using namespace Qt::StringLiterals; @@ -23,8 +23,8 @@ bool MediaMessageFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex { const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - if (index.data(MessageEventModel::MediaInfoRole).toMap()["mimeType"_L1].toString().contains("image"_L1) - || index.data(MessageEventModel::MediaInfoRole).toMap()["mimeType"_L1].toString().contains("video"_L1)) { + if (index.data(TimelineMessageModel::MediaInfoRole).toMap()["mimeType"_L1].toString().contains("image"_L1) + || index.data(TimelineMessageModel::MediaInfoRole).toMap()["mimeType"_L1].toString().contains("video"_L1)) { return true; } return false; @@ -34,21 +34,21 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const { // We need to catch this one and return true if the next media object was // on a different day. - if (role == MessageEventModel::ShowSectionRole) { - const auto day = mapToSource(index).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date(); - const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(MessageEventModel::TimeRole).toDateTime().toLocalTime().date(); + if (role == TimelineMessageModel::ShowSectionRole) { + const auto day = mapToSource(index).data(TimelineMessageModel::TimeRole).toDateTime().toLocalTime().date(); + const auto previousEventDay = mapToSource(this->index(index.row() + 1, 0)).data(TimelineMessageModel::TimeRole).toDateTime().toLocalTime().date(); return day != previousEventDay; } // Catch and force the author to be shown for all rows - if (role == MessageEventModel::ContentModelRole) { - const auto model = qvariant_cast(mapToSource(index).data(MessageEventModel::ContentModelRole)); + if (role == TimelineMessageModel::ContentModelRole) { + const auto model = qvariant_cast(mapToSource(index).data(TimelineMessageModel::ContentModelRole)); if (model != nullptr) { model->setShowAuthor(true); } return QVariant::fromValue(model); } - QVariantMap mediaInfo = mapToSource(index).data(MessageEventModel::MediaInfoRole).toMap(); + QVariantMap mediaInfo = mapToSource(index).data(TimelineMessageModel::MediaInfoRole).toMap(); if (role == TempSourceRole) { return mediaInfo[u"tempInfo"_s].toMap()[u"source"_s].toUrl(); @@ -70,9 +70,9 @@ QVariant MediaMessageFilterModel::data(const QModelIndex &index, int role) const } if (role == SourceRole) { if (isVideo) { - auto progressInfo = mapToSource(index).data(MessageEventModel::ProgressInfoRole).value(); + auto progressInfo = mapToSource(index).data(TimelineMessageModel::ProgressInfoRole).value(); if (progressInfo.completed()) { - return mapToSource(index).data(MessageEventModel::ProgressInfoRole).value().localPath; + return mapToSource(index).data(TimelineMessageModel::ProgressInfoRole).value().localPath; } } else { return mediaInfo[u"source"_s].toUrl(); diff --git a/src/models/mediamessagefiltermodel.h b/src/models/mediamessagefiltermodel.h index 8903f8d2f..9d8125f2f 100644 --- a/src/models/mediamessagefiltermodel.h +++ b/src/models/mediamessagefiltermodel.h @@ -13,9 +13,9 @@ class MessageFilterModel; /** * @class MediaMessageFilterModel * - * This model filters a MessageEventModel for image and video messages. + * This model filters a TimelineMessageModel for image and video messages. * - * @sa MessageEventModel + * @sa TimelineMessageModel */ class MediaMessageFilterModel : public QSortFilterProxyModel { diff --git a/src/models/messagecontentmodel.cpp b/src/models/messagecontentmodel.cpp index d08fa92e4..0369727a2 100644 --- a/src/models/messagecontentmodel.cpp +++ b/src/models/messagecontentmodel.cpp @@ -311,20 +311,10 @@ QVariant MessageContentModel::data(const QModelIndex &index, int role) const return event.first->displayId(); } if (role == TimeRole) { - const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) { - return event.first->transactionId() == pendingEvent->transactionId(); - }); - - auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated(); - return EventHandler::time(event.first, m_currentState == Pending, lastUpdated); + return EventHandler::time(m_room, event.first, m_currentState == Pending); } if (role == TimeStringRole) { - const auto pendingIt = std::find_if(m_room->pendingEvents().cbegin(), m_room->pendingEvents().cend(), [event](const PendingEventItem &pendingEvent) { - return event.first->transactionId() == pendingEvent->transactionId(); - }); - - auto lastUpdated = pendingIt == m_room->pendingEvents().cend() ? QDateTime() : pendingIt->lastUpdated(); - return EventHandler::timeString(event.first, u"hh:mm"_s, m_currentState == Pending, lastUpdated); + return EventHandler::timeString(m_room, event.first, u"hh:mm"_s, m_currentState == Pending); } if (role == AuthorRole) { return QVariant::fromValue(m_eventSenderObject.get()); diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp deleted file mode 100644 index addbb41d5..000000000 --- a/src/models/messageeventmodel.cpp +++ /dev/null @@ -1,722 +0,0 @@ -// SPDX-FileCopyrightText: 2018-2019 Black Hat -// SPDX-License-Identifier: GPL-3.0-only - -#include "messageeventmodel.h" -#include "messagecomponenttype.h" -#include "messageeventmodel_logging.h" - -#include "neochatconfig.h" - -#include -#include -#include -#include -#include -#include -#if Quotient_VERSION_MINOR > 9 -#include -#endif - -#include -#include -#include - -#include -#include - -#include "enums/delegatetype.h" -#include "eventhandler.h" -#include "events/pollevent.h" -#include "models/messagefiltermodel.h" -#include "models/reactionmodel.h" -#include "texthandler.h" - -using namespace Quotient; - -QHash MessageEventModel::roleNames() const -{ - QHash roles = QAbstractItemModel::roleNames(); - roles[DelegateTypeRole] = "delegateType"; - roles[EventIdRole] = "eventId"; - roles[TimeRole] = "time"; - roles[SectionRole] = "section"; - roles[AuthorRole] = "author"; - roles[HighlightRole] = "isHighlighted"; - roles[SpecialMarksRole] = "marks"; - roles[ProgressInfoRole] = "progressInfo"; - roles[IsThreadedRole] = "isThreaded"; - roles[ThreadRootRole] = "threadRoot"; - roles[ShowSectionRole] = "showSection"; - roles[ReadMarkersRole] = "readMarkers"; - roles[ShowReadMarkersRole] = "showReadMarkers"; - roles[ReactionRole] = "reaction"; - roles[ShowReactionsRole] = "showReactions"; - roles[VerifiedRole] = "verified"; - roles[AuthorDisplayNameRole] = "authorDisplayName"; - roles[IsRedactedRole] = "isRedacted"; - roles[GenericDisplayRole] = "genericDisplay"; - roles[IsPendingRole] = "isPending"; - roles[ContentModelRole] = "contentModel"; - roles[MediaInfoRole] = "mediaInfo"; - roles[IsEditableRole] = "isEditable"; - return roles; -} - -MessageEventModel::MessageEventModel(QObject *parent) - : QAbstractListModel(parent) -{ - connect(this, &MessageEventModel::modelAboutToBeReset, this, [this]() { - resetting = true; - }); - connect(this, &MessageEventModel::modelReset, this, [this]() { - resetting = false; - }); - - connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() { - beginResetModel(); - endResetModel(); - }); -} - -NeoChatRoom *MessageEventModel::room() const -{ - return m_currentRoom; -} - -void MessageEventModel::setRoom(NeoChatRoom *room) -{ - if (room == m_currentRoom) { - return; - } - - if (m_currentRoom) { - // HACK: Reset the model to a null room first to make sure QML dismantles - // last room's objects before the room is actually changed - beginResetModel(); - m_currentRoom->disconnect(this); - m_currentRoom = nullptr; - endResetModel(); - - // Don't clear the member objects until the model has been fully reset and all - // refs cleared. - m_memberObjects.clear(); - m_contentModels.clear(); - m_reactionModels.clear(); - m_readMarkerModels.clear(); - } - - beginResetModel(); - m_currentRoom = room; - Q_EMIT roomChanged(); - if (room) { - m_lastReadEventIndex = QPersistentModelIndex(QModelIndex()); - room->setDisplayed(); - - for (auto event = m_currentRoom->messageEvents().begin(); event != m_currentRoom->messageEvents().end(); ++event) { - createEventObjects(&*event->viewAs()); - if (event->event()->is()) { - m_currentRoom->createPollHandler(eventCast(event->event())); - } - } - - if (m_currentRoom->timelineSize() < 10 && !room->allHistoryLoaded()) { - room->getPreviousContent(50); - } - lastReadEventId = room->lastFullyReadEventId(); - - connect(m_currentRoom, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) { - for (auto &&event : events) { - createEventObjects(event.get()); - if (event->is()) { - m_currentRoom->createPollHandler(eventCast(event.get())); - } - } - m_initialized = true; - beginInsertRows({}, timelineBaseIndex(), timelineBaseIndex() + int(events.size()) - 1); - }); - connect(m_currentRoom, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) { - for (auto &event : events) { - createEventObjects(event.get()); - if (event->is()) { - m_currentRoom->createPollHandler(eventCast(event.get())); - } - } - if (rowCount() > 0) { - rowBelowInserted = rowCount() - 1; // See #312 - } - m_initialized = true; - beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1); - }); - connect(m_currentRoom, &Room::addedMessages, this, [this](int lowest, int biggest) { - if (m_initialized) { - endInsertRows(); - } - if (!m_lastReadEventIndex.isValid()) { - // no read marker, so see if we need to create one. - moveReadMarker(m_currentRoom->lastFullyReadEventId()); - } - if (biggest < m_currentRoom->maxTimelineIndex()) { - auto rowBelowInserted = m_currentRoom->maxTimelineIndex() - biggest + timelineBaseIndex() - 1; - refreshEventRoles(rowBelowInserted, {ContentModelRole}); - } - for (auto i = m_currentRoom->maxTimelineIndex() - biggest; i <= m_currentRoom->maxTimelineIndex() - lowest; ++i) { - refreshLastUserEvents(i); - } - }); -#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 0) - connect(m_currentRoom, &Room::pendingEventAdded, this, [this](const Quotient::RoomEvent *event) { - m_initialized = true; - createEventObjects(event, true); - beginInsertRows({}, 0, 0); - endInsertRows(); - }); -#else - connect(m_currentRoom, &Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) { - m_initialized = true; - createEventObjects(event, true); - beginInsertRows({}, 0, 0); - }); - connect(m_currentRoom, &Room::pendingEventAdded, this, &MessageEventModel::endInsertRows); -#endif - connect(m_currentRoom, &Room::pendingEventAboutToMerge, this, [this](RoomEvent *, int i) { - Q_EMIT dataChanged(index(i, 0), index(i, 0), {IsPendingRole}); - if (i == 0) { - return; // No need to move anything, just refresh - } - - movingEvent = true; - // Reverse i because row 0 is bottommost in the model - const auto row = timelineBaseIndex() - i - 1; - beginMoveRows({}, row, row, {}, timelineBaseIndex()); - }); - connect(m_currentRoom, &Room::pendingEventMerged, this, [this] { - if (movingEvent) { - endMoveRows(); - movingEvent = false; - } - fullEventRefresh(timelineBaseIndex()); - refreshLastUserEvents(0); - if (timelineBaseIndex() > 0) { // Refresh below, see #312 - refreshEventRoles(timelineBaseIndex() - 1, {ContentModelRole}); - } - }); - connect(m_currentRoom, &Room::pendingEventChanged, this, &MessageEventModel::fullEventRefresh); - connect(m_currentRoom, &Room::pendingEventAboutToDiscard, this, [this](int i) { - beginRemoveRows({}, i, i); - }); - connect(m_currentRoom, &Room::pendingEventDiscarded, this, &MessageEventModel::endRemoveRows); - connect(m_currentRoom, &Room::fullyReadMarkerMoved, this, [this](const QString &fromEventId, const QString &toEventId) { - Q_UNUSED(fromEventId); - moveReadMarker(toEventId); - }); - connect(m_currentRoom, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) { - createEventObjects(newEvent); - }); - connect(m_currentRoom, &Room::updatedEvent, this, [this](const QString &eventId) { - if (eventId.isEmpty()) { // How did we get here? - return; - } - const auto eventIt = m_currentRoom->findInTimeline(eventId); - if (eventIt != m_currentRoom->historyEdge()) { - createEventObjects(eventIt->event()); - if (eventIt->event()->is()) { - m_currentRoom->createPollHandler(eventCast(eventIt->event())); - } - } - refreshEventRoles(eventId, {Qt::DisplayRole}); - }); - connect(m_currentRoom, &Room::changed, this, [this](Room::Changes changes) { - if (changes.testFlag(Quotient::Room::Change::Other)) { - // this is slow - for (auto it = m_currentRoom->messageEvents().rbegin(); it != m_currentRoom->messageEvents().rend(); ++it) { - createEventObjects(it->event()); - } - } - }); - connect(m_currentRoom->connection(), &Connection::ignoredUsersListChanged, this, [this] { - beginResetModel(); - endResetModel(); - }); - - qCDebug(MessageEvent) << "Connected to room" << room->id() << "as" << room->localMember().id(); - } else { - lastReadEventId.clear(); - } - endResetModel(); - - // After reset put a read marker in if required. - // This is needed when changing back to a room that has already loaded messages. - if (room) { - moveReadMarker(m_currentRoom->lastFullyReadEventId()); - } -} - -void MessageEventModel::fullEventRefresh(int row) -{ - auto roles = roleNames().keys(); - // The author of an event never changes so should only be updated when a member - // changed signal is emitted. - // This also avoids any race conditions where a member is updating and this refresh - // tries to access a member event that has already been deleted. - roles.removeAll(AuthorRole); - refreshEventRoles(row, roles); -} - -int MessageEventModel::timelineBaseIndex() const -{ - return m_currentRoom ? int(m_currentRoom->pendingEvents().size()) : 0; -} - -void MessageEventModel::refreshEventRoles(int row, const QList &roles) -{ - const auto idx = index(row); - Q_EMIT dataChanged(idx, idx, roles); -} - -void MessageEventModel::moveReadMarker(const QString &toEventId) -{ - const auto timelineIt = m_currentRoom->findInTimeline(toEventId); - if (timelineIt == m_currentRoom->historyEdge()) { - return; - } - int newRow = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex(); - - if (!m_lastReadEventIndex.isValid()) { - // Not valid index means we don't display any marker yet, in this case - // we create the new index and insert the row in case the read marker - // need to be displayed. - if (newRow > timelineBaseIndex()) { - // The user didn't read all the messages yet. - m_initialized = true; - beginInsertRows({}, newRow, newRow); - m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0)); - endInsertRows(); - return; - } - // The user read all the messages and we didn't display any read marker yet - // => do nothing - return; - } - if (newRow <= timelineBaseIndex()) { - // The user read all the messages => remove read marker - beginRemoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row()); - m_lastReadEventIndex = QModelIndex(); - endRemoveRows(); - return; - } - - // The user didn't read all the messages yet but moved the reader marker. - beginMoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row(), {}, newRow); - m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0)); - endMoveRows(); -} - -int MessageEventModel::refreshEventRoles(const QString &id, const QList &roles) -{ - // On 64-bit platforms, difference_type for std containers is long long - // but Qt uses int throughout its interfaces; hence casting to int below. - int row = -1; - // First try pendingEvents because it is almost always very short. - const auto pendingIt = m_currentRoom->findPendingEvent(id); - if (pendingIt != m_currentRoom->pendingEvents().end()) { - row = int(pendingIt - m_currentRoom->pendingEvents().begin()); - } else { - const auto timelineIt = m_currentRoom->findInTimeline(id); - if (timelineIt == m_currentRoom->historyEdge()) { - return -1; - } - row = int(timelineIt - m_currentRoom->messageEvents().rbegin()) + timelineBaseIndex(); - if (data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::Other) { - row++; - } - } - refreshEventRoles(row, roles); - return row; -} - -inline bool hasValidTimestamp(const Quotient::TimelineItem &ti) -{ - return ti->originTimestamp().isValid(); -} - -void MessageEventModel::refreshLastUserEvents(int baseTimelineRow) -{ - if (!m_currentRoom || m_currentRoom->timelineSize() <= baseTimelineRow) { - return; - } - - const auto &timelineBottom = m_currentRoom->messageEvents().rbegin(); - const auto &lastSender = (*(timelineBottom + baseTimelineRow))->senderId(); - const auto limit = timelineBottom + std::min(baseTimelineRow + 10, m_currentRoom->timelineSize()); - for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); it != limit; ++it) { - if ((*it)->senderId() == lastSender) { - fullEventRefresh(it - timelineBottom); - } - } -} - -int MessageEventModel::rowCount(const QModelIndex &parent) const -{ - if (!m_currentRoom || parent.isValid()) { - return 0; - } - - return int(m_currentRoom->pendingEvents().size()) + m_currentRoom->timelineSize() + (m_lastReadEventIndex.isValid() ? 1 : 0); -} - -bool MessageEventModel::canFetchMore(const QModelIndex &parent) const -{ - Q_UNUSED(parent); - - return m_currentRoom && !m_currentRoom->eventsHistoryJob() && !m_currentRoom->allHistoryLoaded(); -} - -void MessageEventModel::fetchMore(const QModelIndex &parent) -{ - Q_UNUSED(parent); - if (m_currentRoom) { - m_currentRoom->getPreviousContent(20); - } -} - -static NeochatRoomMember *emptyNeochatRoomMember = new NeochatRoomMember; - -QVariant MessageEventModel::data(const QModelIndex &idx, int role) const -{ - if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) { - return {}; - } - const auto row = idx.row(); - - if (!m_currentRoom || row < 0 || row >= rowCount()) { - return {}; - }; - - bool isPending = row < timelineBaseIndex(); - - if (m_lastReadEventIndex.row() == row) { - switch (role) { - case DelegateTypeRole: - return DelegateType::ReadMarker; - case TimeRole: { - const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime(); - static const KFormat format; - return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat); - } - case SpecialMarksRole: - // Check if all the earlier events in the timeline are hidden. If so hide this. - for (auto r = row - 1; r >= 0; --r) { - const auto specialMark = index(r).data(SpecialMarksRole); - if (!(specialMark == EventStatus::Hidden || specialMark == EventStatus::Replaced)) { - return EventStatus::Normal; - } - } - return EventStatus::Hidden; - } - return {}; - } - - const auto timelineIt = m_currentRoom->messageEvents().crbegin() - + std::max(0, row - timelineBaseIndex() - (m_lastReadEventIndex.isValid() && m_lastReadEventIndex.row() < row ? 1 : 0)); - const auto pendingIt = m_currentRoom->pendingEvents().crbegin() + std::min(row, timelineBaseIndex()); - const auto &evt = isPending ? **pendingIt : **timelineIt; - - if (role == Qt::DisplayRole) { - return EventHandler::richBody(m_currentRoom, &evt); - } - - if (role == ContentModelRole) { - QString modelId; - if (!evt.id().isEmpty() && m_contentModels.contains(evt.id())) { - modelId = evt.id(); - } else if (!evt.transactionId().isEmpty() && m_contentModels.contains(evt.transactionId())) { - modelId = evt.transactionId(); - } - if (!modelId.isEmpty()) { - return QVariant::fromValue(m_contentModels.at(modelId).get()); - } - return {}; - } - - if (role == GenericDisplayRole) { - return EventHandler::genericBody(m_currentRoom, &evt); - } - - if (role == DelegateTypeRole) { - return DelegateType::typeForEvent(evt); - } - - if (role == AuthorRole) { - QString mId; - if (isPending) { - mId = m_currentRoom->localMember().id(); - } else { - mId = evt.senderId(); - } - - if (!m_memberObjects.contains(mId)) { - return QVariant::fromValue(emptyNeochatRoomMember); - } - - return QVariant::fromValue(m_memberObjects.at(mId).get()); - } - - if (role == HighlightRole) { - return EventHandler::isHighlighted(m_currentRoom, &evt); - } - - if (role == SpecialMarksRole) { - if (isPending) { - // A pending event with an m.new_content key will be merged into the - // original event so don't show. - if (evt.contentJson().contains("m.new_content"_L1)) { - return EventStatus::Hidden; - } - return pendingIt->deliveryStatus(); - } - - if (EventHandler::isHidden(m_currentRoom, &evt)) { - return EventStatus::Hidden; - } - - auto roomMessageEvent = eventCast(&evt); -#if Quotient_VERSION_MINOR > 9 - if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_currentRoom->threads().contains(evt.id()))) { - const auto &thread = m_currentRoom->threads().value(roomMessageEvent->isThreaded() ? roomMessageEvent->threadRootEventId() : evt.id()); - if (thread.latestEventId != evt.id()) { - return EventStatus::Hidden; - } - } -#else - if (roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->threadRootEventId() != evt.id() && NeoChatConfig::threads()) { - return EventStatus::Hidden; - } -#endif - - return EventStatus::Normal; - } - - if (role == EventIdRole) { - return evt.displayId(); - } - - if (role == ProgressInfoRole) { - if (auto e = eventCast(&evt)) { - if (e->has()) { - return QVariant::fromValue(m_currentRoom->cachedFileTransferInfo(&evt)); - } - } - if (eventCast(&evt)) { - return QVariant::fromValue(m_currentRoom->cachedFileTransferInfo(&evt)); - } - } - - if (role == TimeRole) { - auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime(); - return EventHandler::time(&evt, isPending, lastUpdated); - } - - if (role == SectionRole) { - auto lastUpdated = isPending ? pendingIt->lastUpdated() : QDateTime(); - return EventHandler::timeString(&evt, true, QLocale::ShortFormat, isPending, lastUpdated); - } - - if (role == IsThreadedRole) { - if (auto roomMessageEvent = eventCast(&evt)) { - return roomMessageEvent->isThreaded(); - } - return {}; - } - - if (role == ThreadRootRole) { - auto roomMessageEvent = eventCast(&evt); - if (roomMessageEvent && roomMessageEvent->isThreaded()) { - return roomMessageEvent->threadRootEventId(); - } - return {}; - } - - if (role == ShowSectionRole) { - for (auto r = row + 1; r < rowCount(); ++r) { - auto i = index(r); - // Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved. - // While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index. - // See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows - if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) { - const auto day = data(idx, TimeRole).toDateTime().toLocalTime().date().dayOfYear(); - const auto previousEventDay = data(i, TimeRole).toDateTime().toLocalTime().date().dayOfYear(); - return day != previousEventDay; - } - } - - return false; - } - - if (role == ReadMarkersRole) { - if (m_readMarkerModels.contains(evt.id())) { - return QVariant::fromValue(m_readMarkerModels[evt.id()].get()); - } else { - return QVariantList(); - } - } - - if (role == ShowReadMarkersRole) { - return m_readMarkerModels.contains(evt.id()); - } - - if (role == ReactionRole) { - if (m_reactionModels.contains(evt.id())) { - return QVariant::fromValue(m_reactionModels[evt.id()].data()); - } else { - return QVariantList(); - } - } - - if (role == ShowReactionsRole) { - return m_reactionModels.contains(evt.id()); - } - - if (role == VerifiedRole) { - if (evt.originalEvent()) { - auto encrypted = dynamic_cast(evt.originalEvent()); - Q_ASSERT(encrypted); - return m_currentRoom->connection()->isVerifiedSession(encrypted->sessionId().toLatin1()); - } - return false; - } - - if (role == AuthorDisplayNameRole) { - return EventHandler::authorDisplayName(m_currentRoom, &evt, isPending); - } - - if (role == IsRedactedRole) { - return evt.isRedacted(); - } - - if (role == IsPendingRole) { - return row < static_cast(m_currentRoom->pendingEvents().size()); - } - - if (role == MediaInfoRole) { - return EventHandler::mediaInfo(m_currentRoom, &evt); - } - - if (role == IsEditableRole) { - return MessageComponentType::typeForEvent(evt) == MessageComponentType::Text && evt.senderId() == m_currentRoom->localMember().id(); - } - - return {}; -} - -int MessageEventModel::eventIdToRow(const QString &eventID) const -{ - if (m_currentRoom == nullptr) { - return -1; - } - - const auto it = m_currentRoom->findInTimeline(eventID); - if (it == m_currentRoom->historyEdge()) { - // qWarning() << "Trying to find inexistent event:" << eventID; - return -1; - } - return it - m_currentRoom->messageEvents().rbegin() + timelineBaseIndex(); -} - -void MessageEventModel::createEventObjects(const Quotient::RoomEvent *event, bool isPending) -{ - if (event == nullptr) { - return; - } - - auto eventId = event->id(); - auto senderId = event->senderId(); - if (eventId.isEmpty()) { - eventId = event->transactionId(); - } - // 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_currentRoom->localMember().id(); - } - - if (!m_memberObjects.contains(senderId)) { - m_memberObjects[senderId] = std::unique_ptr(new NeochatRoomMember(m_currentRoom, senderId)); - } - - if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) { - if (!event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) { - m_contentModels[eventId] = std::unique_ptr(new MessageContentModel(m_currentRoom, eventId, false, isPending)); - } - } - - const auto roomMessageEvent = eventCast(event); - if (roomMessageEvent && roomMessageEvent->isThreaded() && !m_threadModels.contains(roomMessageEvent->threadRootEventId())) { - m_threadModels[roomMessageEvent->threadRootEventId()] = - QSharedPointer(new ThreadModel(roomMessageEvent->threadRootEventId(), 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)) { - // If a model already exists but now has no reactions remove it - if (m_readMarkerModels[eventId]->rowCount() <= 0) { - m_readMarkerModels.remove(eventId); - if (!resetting) { - refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole}); - } - } - } else { - auto memberIds = m_currentRoom->userIdsAtEvent(eventId); - memberIds.remove(m_currentRoom->localMember().id()); - if (memberIds.size() > 0) { - // If a model doesn't exist and there are reactions add it. - auto newModel = QSharedPointer(new ReadMarkerModel(eventId, m_currentRoom)); - if (newModel->rowCount() > 0) { - m_readMarkerModels[eventId] = newModel; - if (!resetting) { - refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole}); - } - } - } - } - - if (const auto roomEvent = eventCast(event)) { - // ReactionModel handles updates to add and remove reactions, we only need to - // handle adding and removing whole models here. - if (m_reactionModels.contains(eventId)) { - // If a model already exists but now has no reactions remove it - if (m_reactionModels[eventId]->rowCount() <= 0) { - m_reactionModels.remove(eventId); - if (!resetting) { - refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); - } - } - } else { - if (m_currentRoom->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) { - // If a model doesn't exist and there are reactions add it. - auto reactionModel = QSharedPointer(new ReactionModel(roomEvent, m_currentRoom)); - if (reactionModel->rowCount() > 0) { - m_reactionModels[eventId] = reactionModel; - if (!resetting) { - refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); - } - } - } - } - } -} - -bool MessageEventModel::event(QEvent *event) -{ - if (event->type() == QEvent::ApplicationPaletteChange) { - Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole}); - } - return QObject::event(event); -} - -ThreadModel *MessageEventModel::threadModelForRootId(const QString &threadRootId) const -{ - return m_threadModels[threadRootId].data(); -} - -#include "moc_messageeventmodel.cpp" diff --git a/src/models/messagefiltermodel.cpp b/src/models/messagefiltermodel.cpp index b5c7b3221..2eede1c8c 100644 --- a/src/models/messagefiltermodel.cpp +++ b/src/models/messagefiltermodel.cpp @@ -10,6 +10,7 @@ #include "messagecontentmodel.h" #include "neochatconfig.h" #include "neochatroommember.h" +#include "timelinemessagemodel.h" using namespace Quotient; @@ -49,18 +50,18 @@ bool MessageFilterModel::eventIsVisible(int sourceRow, const QModelIndex &source const QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); // Don't show redacted (i.e. deleted) messages. - if (index.data(MessageEventModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) { + if (index.data(TimelineMessageModel::IsRedactedRole).toBool() && !NeoChatConfig::self()->showDeletedMessages()) { return false; } // Don't show hidden or replaced messages. - const int specialMarks = index.data(MessageEventModel::SpecialMarksRole).toInt(); + const int specialMarks = index.data(TimelineMessageModel::SpecialMarksRole).toInt(); if (specialMarks == EventStatus::Hidden || specialMarks == EventStatus::Replaced) { return false; } // Don't show events with an unknown type. - const auto eventType = index.data(MessageEventModel::DelegateTypeRole).toInt(); + const auto eventType = index.data(TimelineMessageModel::DelegateTypeRole).toInt(); if (eventType == DelegateType::Other) { return false; } @@ -69,8 +70,8 @@ bool MessageFilterModel::eventIsVisible(int sourceRow, const QModelIndex &source // same day as they will be grouped as a single delegate. const bool notLastRow = sourceRow < sourceModel()->rowCount() - 1; const bool previousEventIsState = - notLastRow ? sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), MessageEventModel::DelegateTypeRole) == DelegateType::State : false; - const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), MessageEventModel::ShowSectionRole).toBool(); + notLastRow ? sourceModel()->data(sourceModel()->index(sourceRow + 1, 0), TimelineMessageModel::DelegateTypeRole) == DelegateType::State : false; + const bool newDay = sourceModel()->data(sourceModel()->index(sourceRow, 0), TimelineMessageModel::ShowSectionRole).toBool(); if (eventType == DelegateType::State && notLastRow && previousEventIsState && !newDay) { return false; } @@ -80,7 +81,7 @@ bool MessageFilterModel::eventIsVisible(int sourceRow, const QModelIndex &source QVariant MessageFilterModel::data(const QModelIndex &index, int role) const { - if (role == MessageEventModel::DelegateTypeRole && NeoChatConfig::self()->showAllEvents()) { + if (role == TimelineMessageModel::DelegateTypeRole && NeoChatConfig::self()->showAllEvents()) { if (!eventIsVisible(index.row(), index.parent())) { return DelegateType::Other; } @@ -92,8 +93,8 @@ QVariant MessageFilterModel::data(const QModelIndex &index, int role) const return authorList(mapToSource(index).row()); } else if (role == ExcessAuthorsRole) { return excessAuthors(mapToSource(index).row()); - } else if (role == MessageEventModel::ContentModelRole) { - const auto model = qvariant_cast(mapToSource(index).data(MessageEventModel::ContentModelRole)); + } else if (role == TimelineMessageModel::ContentModelRole) { + const auto model = qvariant_cast(mapToSource(index).data(TimelineMessageModel::ContentModelRole)); if (model != nullptr && !showAuthor(index)) { model->setShowAuthor(false); } @@ -119,12 +120,12 @@ bool MessageFilterModel::showAuthor(QModelIndex index) const // Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved. // While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index. // See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows - if (data(i, MessageEventModel::SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) { - return data(i, MessageEventModel::AuthorRole) != data(index, MessageEventModel::AuthorRole) - || data(i, MessageEventModel::DelegateTypeRole) == DelegateType::State - || data(i, MessageEventModel::TimeRole).toDateTime().msecsTo(data(index, MessageEventModel::TimeRole).toDateTime()) > 600000 - || data(i, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day() - != data(index, MessageEventModel::TimeRole).toDateTime().toLocalTime().date().day(); + if (data(i, TimelineMessageModel::SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) { + return data(i, TimelineMessageModel::AuthorRole) != data(index, TimelineMessageModel::AuthorRole) + || data(i, TimelineMessageModel::DelegateTypeRole) == DelegateType::State + || data(i, TimelineMessageModel::TimeRole).toDateTime().msecsTo(data(index, TimelineMessageModel::TimeRole).toDateTime()) > 600000 + || data(i, TimelineMessageModel::TimeRole).toDateTime().toLocalTime().date().day() + != data(index, TimelineMessageModel::TimeRole).toDateTime().toLocalTime().date().day(); } } @@ -135,12 +136,12 @@ QString MessageFilterModel::aggregateEventToString(int sourceRow) const { QString aggregateString; for (int i = sourceRow; i >= 0; i--) { - aggregateString += sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::GenericDisplayRole).toString(); + aggregateString += sourceModel()->data(sourceModel()->index(i, 0), TimelineMessageModel::GenericDisplayRole).toString(); aggregateString += ", "_L1; - QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); + QVariant nextAuthor = sourceModel()->data(sourceModel()->index(i, 0), TimelineMessageModel::AuthorRole); if (i > 0 - && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event - || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible + && (sourceModel()->data(sourceModel()->index(i - 1, 0), TimelineMessageModel::DelegateTypeRole) != DelegateType::State // If it's not a state event + || sourceModel()->data(sourceModel()->index(i - 1, 0), TimelineMessageModel::ShowSectionRole).toBool() // or the section needs to be visible )) { break; } @@ -158,14 +159,14 @@ QVariantList MessageFilterModel::stateEventsList(int sourceRow) const QVariantList stateEvents; for (int i = sourceRow; i >= 0; i--) { auto nextState = QVariantMap{ - {u"author"_s, sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole)}, - {u"authorDisplayName"_s, sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorDisplayNameRole).toString()}, + {u"author"_s, sourceModel()->data(sourceModel()->index(i, 0), TimelineMessageModel::AuthorRole)}, + {u"authorDisplayName"_s, sourceModel()->data(sourceModel()->index(i, 0), TimelineMessageModel::AuthorDisplayNameRole).toString()}, {u"text"_s, sourceModel()->data(sourceModel()->index(i, 0), Qt::DisplayRole).toString()}, }; stateEvents.append(nextState); if (i > 0 - && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event - || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible + && (sourceModel()->data(sourceModel()->index(i - 1, 0), TimelineMessageModel::DelegateTypeRole) != DelegateType::State // If it's not a state event + || sourceModel()->data(sourceModel()->index(i - 1, 0), TimelineMessageModel::ShowSectionRole).toBool() // or the section needs to be visible )) { break; } @@ -177,13 +178,13 @@ QVariantList MessageFilterModel::authorList(int sourceRow) const { QVariantList uniqueAuthors; for (int i = sourceRow; i >= 0; i--) { - QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); + QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), TimelineMessageModel::AuthorRole); if (!uniqueAuthors.contains(nextAvatar)) { uniqueAuthors.append(nextAvatar); } if (i > 0 - && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event - || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible + && (sourceModel()->data(sourceModel()->index(i - 1, 0), TimelineMessageModel::DelegateTypeRole) != DelegateType::State // If it's not a state event + || sourceModel()->data(sourceModel()->index(i - 1, 0), TimelineMessageModel::ShowSectionRole).toBool() // or the section needs to be visible )) { break; } @@ -199,13 +200,13 @@ QString MessageFilterModel::excessAuthors(int row) const { QVariantList uniqueAuthors; for (int i = row; i >= 0; i--) { - QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), MessageEventModel::AuthorRole); + QVariant nextAvatar = sourceModel()->data(sourceModel()->index(i, 0), TimelineMessageModel::AuthorRole); if (!uniqueAuthors.contains(nextAvatar)) { uniqueAuthors.append(nextAvatar); } if (i > 0 - && (sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::DelegateTypeRole) != DelegateType::State // If it's not a state event - || sourceModel()->data(sourceModel()->index(i - 1, 0), MessageEventModel::ShowSectionRole).toBool() // or the section needs to be visible + && (sourceModel()->data(sourceModel()->index(i - 1, 0), TimelineMessageModel::DelegateTypeRole) != DelegateType::State // If it's not a state event + || sourceModel()->data(sourceModel()->index(i - 1, 0), TimelineMessageModel::ShowSectionRole).toBool() // or the section needs to be visible )) { break; } diff --git a/src/models/messagefiltermodel.h b/src/models/messagefiltermodel.h index 1791af578..0c9aa2881 100644 --- a/src/models/messagefiltermodel.h +++ b/src/models/messagefiltermodel.h @@ -6,7 +6,7 @@ #include #include -#include "messageeventmodel.h" +#include "timelinemessagemodel.h" #include "timelinemodel.h" /** @@ -30,7 +30,7 @@ public: * @brief Defines the model roles. */ enum Roles { - AggregateDisplayRole = MessageEventModel::LastRole + 1, /**< Single line aggregation of all the state events. */ + AggregateDisplayRole = TimelineMessageModel::LastRole + 1, /**< Single line aggregation of all the state events. */ StateEventsRole, /**< List of state events in the aggregated state. */ AuthorListRole, /**< List of the first 5 unique authors of the aggregated state event. */ ExcessAuthorsRole, /**< The number of unique authors beyond the first 5. */ diff --git a/src/models/messagemodel.cpp b/src/models/messagemodel.cpp new file mode 100644 index 000000000..7f565f295 --- /dev/null +++ b/src/models/messagemodel.cpp @@ -0,0 +1,530 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "messagemodel.h" + +#include "neochatconfig.h" + +#include +#include +#if Quotient_VERSION_MINOR > 9 +#include +#endif + +#include + +#include "enums/delegatetype.h" +#include "enums/messagecomponenttype.h" +#include "eventhandler.h" +#include "events/pollevent.h" +#include "models/reactionmodel.h" + +using namespace Quotient; + +MessageModel::MessageModel(QObject *parent) + : QAbstractListModel(parent) +{ + connect(this, &MessageModel::newEventAdded, this, &MessageModel::createEventObjects); + + connect(this, &MessageModel::modelAboutToBeReset, this, [this]() { + resetting = true; + }); + connect(this, &MessageModel::modelReset, this, [this]() { + resetting = false; + }); + + connect(NeoChatConfig::self(), &NeoChatConfig::ThreadsChanged, this, [this]() { + beginResetModel(); + endResetModel(); + }); +} + +NeoChatRoom *MessageModel::room() const +{ + return m_room; +} + +void MessageModel::setRoom(NeoChatRoom *room) +{ + if (room == m_room) { + return; + } + + clearModel(); + + beginResetModel(); + m_room = room; + Q_EMIT roomChanged(); + endResetModel(); +} + +int MessageModel::timelineServerIndex() const +{ + return 0; +} + +std::optional> MessageModel::getEventForIndex(QModelIndex index) const +{ + Q_UNUSED(index) + return std::nullopt; +} + +static NeochatRoomMember *emptyNeochatRoomMember = new NeochatRoomMember; + +QVariant MessageModel::data(const QModelIndex &idx, int role) const +{ + if (!checkIndex(idx, QAbstractItemModel::CheckIndexOption::IndexIsValid)) { + return {}; + } + const auto row = idx.row(); + + if (!m_room || row < 0 || row >= rowCount()) { + return {}; + }; + + if (m_lastReadEventIndex.row() == row) { + switch (role) { + case DelegateTypeRole: + return DelegateType::ReadMarker; + case TimeRole: { + const QDateTime eventDate = data(index(m_lastReadEventIndex.row() + 1, 0), TimeRole).toDateTime().toLocalTime(); + static const KFormat format; + return format.formatRelativeDateTime(eventDate, QLocale::ShortFormat); + } + case SpecialMarksRole: + // Check if all the earlier events in the timeline are hidden. If so hide this. + for (auto r = row - 1; r >= 0; --r) { + const auto specialMark = index(r).data(SpecialMarksRole); + if (!(specialMark == EventStatus::Hidden || specialMark == EventStatus::Replaced)) { + return EventStatus::Normal; + } + } + return EventStatus::Hidden; + } + return {}; + } + + bool isPending = row < timelineServerIndex(); + + const auto event = getEventForIndex(idx); + if (!event.has_value()) { + return {}; + } + + if (role == Qt::DisplayRole) { + return EventHandler::richBody(m_room, &event.value().get()); + } + + if (role == ContentModelRole) { + QString modelId; + if (!event->get().id().isEmpty() && m_contentModels.contains(event->get().id())) { + modelId = event.value().get().id(); + } else if (!event.value().get().transactionId().isEmpty() && m_contentModels.contains(event.value().get().transactionId())) { + modelId = event.value().get().transactionId(); + } + if (!modelId.isEmpty()) { + return QVariant::fromValue(m_contentModels.at(modelId).get()); + } + return {}; + } + + if (role == GenericDisplayRole) { + return EventHandler::genericBody(m_room, &event.value().get()); + } + + if (role == DelegateTypeRole) { + return DelegateType::typeForEvent(event.value().get()); + } + + if (role == AuthorRole) { + QString mId; + if (isPending) { + mId = m_room->localMember().id(); + } else { + mId = event.value().get().senderId(); + } + + if (!m_memberObjects.contains(mId)) { + return QVariant::fromValue(emptyNeochatRoomMember); + } + + return QVariant::fromValue(m_memberObjects.at(mId).get()); + } + + if (role == HighlightRole) { + return EventHandler::isHighlighted(m_room, &event.value().get()); + } + + if (role == SpecialMarksRole) { + if (isPending) { + // A pending event with an m.new_content key will be merged into the + // original event so don't show. + if (event.value().get().contentJson().contains("m.new_content"_L1)) { + return EventStatus::Hidden; + } + const auto pendingIt = m_room->findPendingEvent(event->get().transactionId()); + if (pendingIt == m_room->pendingEvents().end()) { + return EventStatus::Hidden; + } + return pendingIt->deliveryStatus(); + } + + if (EventHandler::isHidden(m_room, &event.value().get())) { + return EventStatus::Hidden; + } + + auto roomMessageEvent = eventCast(&event.value().get()); +#if Quotient_VERSION_MINOR > 9 + if (roomMessageEvent && (roomMessageEvent->isThreaded() || m_room->threads().contains(event.value().get().id()))) { + const auto &thread = m_room->threads().value(roomMessageEvent->isThreaded() ? roomMessageEvent->threadRootEventId() : event.value().get().id()); + if (thread.latestEventId != event.value().get().id()) { + return EventStatus::Hidden; + } + } +#else + if (roomMessageEvent && roomMessageEvent->isThreaded() && roomMessageEvent->threadRootEventId() != event.value().get().id() + && NeoChatConfig::threads()) { + return EventStatus::Hidden; + } +#endif + + return EventStatus::Normal; + } + + if (role == EventIdRole) { + return event.value().get().displayId(); + } + + if (role == ProgressInfoRole) { + if (auto e = eventCast(&event.value().get())) { + if (e->has()) { + return QVariant::fromValue(m_room->cachedFileTransferInfo(&event.value().get())); + } + } + if (eventCast(&event.value().get())) { + return QVariant::fromValue(m_room->cachedFileTransferInfo(&event.value().get())); + } + } + + if (role == TimeRole) { + return EventHandler::time(m_room, &event.value().get(), isPending); + } + + if (role == SectionRole) { + return EventHandler::timeString(m_room, &event.value().get(), true, QLocale::ShortFormat, isPending); + } + + if (role == IsThreadedRole) { + if (auto roomMessageEvent = eventCast(&event.value().get())) { + return roomMessageEvent->isThreaded(); + } + return {}; + } + + if (role == ThreadRootRole) { + auto roomMessageEvent = eventCast(&event.value().get()); + if (roomMessageEvent && roomMessageEvent->isThreaded()) { + return roomMessageEvent->threadRootEventId(); + } + return {}; + } + + if (role == ShowSectionRole) { + for (auto r = row + 1; r < rowCount(); ++r) { + auto i = index(r); + // Note !itemData(i).empty() is a check for instances where rows have been removed, e.g. when the read marker is moved. + // While the row is removed the subsequent row indexes are not changed so we need to skip over the removed index. + // See - https://doc.qt.io/qt-5/qabstractitemmodel.html#beginRemoveRows + if (data(i, SpecialMarksRole) != EventStatus::Hidden && !itemData(i).empty()) { + const auto day = data(idx, TimeRole).toDateTime().toLocalTime().date().dayOfYear(); + const auto previousEventDay = data(i, TimeRole).toDateTime().toLocalTime().date().dayOfYear(); + return day != previousEventDay; + } + } + + return false; + } + + if (role == ReadMarkersRole) { + if (m_readMarkerModels.contains(event.value().get().id())) { + return QVariant::fromValue(m_readMarkerModels[event.value().get().id()].get()); + } else { + return QVariantList(); + } + } + + if (role == ShowReadMarkersRole) { + return m_readMarkerModels.contains(event.value().get().id()); + } + + if (role == ReactionRole) { + if (m_reactionModels.contains(event.value().get().id())) { + return QVariant::fromValue(m_reactionModels[event.value().get().id()].data()); + } else { + return QVariantList(); + } + } + + if (role == ShowReactionsRole) { + return m_reactionModels.contains(event.value().get().id()); + } + + if (role == VerifiedRole) { + if (event.value().get().originalEvent()) { + auto encrypted = dynamic_cast(event.value().get().originalEvent()); + Q_ASSERT(encrypted); + return m_room->connection()->isVerifiedSession(encrypted->sessionId().toLatin1()); + } + return false; + } + + if (role == AuthorDisplayNameRole) { + return EventHandler::authorDisplayName(m_room, &event.value().get(), isPending); + } + + if (role == IsRedactedRole) { + return event.value().get().isRedacted(); + } + + if (role == IsPendingRole) { + return row < static_cast(m_room->pendingEvents().size()); + } + + if (role == MediaInfoRole) { + return EventHandler::mediaInfo(m_room, &event.value().get()); + } + + if (role == IsEditableRole) { + return MessageComponentType::typeForEvent(event.value().get()) == MessageComponentType::Text + && event.value().get().senderId() == m_room->localMember().id(); + } + + return {}; +} + +QHash MessageModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles[DelegateTypeRole] = "delegateType"; + roles[EventIdRole] = "eventId"; + roles[TimeRole] = "time"; + roles[SectionRole] = "section"; + roles[AuthorRole] = "author"; + roles[HighlightRole] = "isHighlighted"; + roles[SpecialMarksRole] = "marks"; + roles[ProgressInfoRole] = "progressInfo"; + roles[IsThreadedRole] = "isThreaded"; + roles[ThreadRootRole] = "threadRoot"; + roles[ShowSectionRole] = "showSection"; + roles[ReadMarkersRole] = "readMarkers"; + roles[ShowReadMarkersRole] = "showReadMarkers"; + roles[ReactionRole] = "reaction"; + roles[ShowReactionsRole] = "showReactions"; + roles[VerifiedRole] = "verified"; + roles[AuthorDisplayNameRole] = "authorDisplayName"; + roles[IsRedactedRole] = "isRedacted"; + roles[GenericDisplayRole] = "genericDisplay"; + roles[IsPendingRole] = "isPending"; + roles[ContentModelRole] = "contentModel"; + roles[MediaInfoRole] = "mediaInfo"; + roles[IsEditableRole] = "isEditable"; + return roles; +} + +int MessageModel::eventIdToRow(const QString &eventID) const +{ + if (m_room == nullptr) { + return -1; + } + + const auto it = m_room->findInTimeline(eventID); + if (it == m_room->historyEdge()) { + // qWarning() << "Trying to find inexistent event:" << eventID; + return -1; + } + return it - m_room->messageEvents().rbegin() + timelineServerIndex(); +} + +void MessageModel::fullEventRefresh(int row) +{ + auto roles = roleNames().keys(); + // The author of an event never changes so should only be updated when a member + // changed signal is emitted. + // This also avoids any race conditions where a member is updating and this refresh + // tries to access a member event that has already been deleted. + roles.removeAll(AuthorRole); + refreshEventRoles(row, roles); +} + +void MessageModel::refreshEventRoles(int row, const QList &roles) +{ + const auto idx = index(row); + Q_EMIT dataChanged(idx, idx, roles); +} + +int MessageModel::refreshEventRoles(const QString &id, const QList &roles) +{ + // On 64-bit platforms, difference_type for std containers is long long + // but Qt uses int throughout its interfaces; hence casting to int below. + int row = -1; + // First try pendingEvents because it is almost always very short. + const auto pendingIt = m_room->findPendingEvent(id); + if (pendingIt != m_room->pendingEvents().end()) { + row = int(pendingIt - m_room->pendingEvents().begin()); + } else { + const auto timelineIt = m_room->findInTimeline(id); + if (timelineIt == m_room->historyEdge()) { + return -1; + } + row = int(timelineIt - m_room->messageEvents().rbegin()) + timelineServerIndex(); + if (data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::ReadMarker || data(index(row, 0), DelegateTypeRole).toInt() == DelegateType::Other) { + row++; + } + } + refreshEventRoles(row, roles); + return row; +} + +void MessageModel::refreshLastUserEvents(int baseTimelineRow) +{ + if (!m_room || m_room->timelineSize() <= baseTimelineRow) { + return; + } + + const auto &timelineBottom = m_room->messageEvents().rbegin(); + const auto &lastSender = (*(timelineBottom + baseTimelineRow))->senderId(); + const auto limit = timelineBottom + std::min(baseTimelineRow + 10, m_room->timelineSize()); + for (auto it = timelineBottom + std::max(baseTimelineRow - 10, 0); it != limit; ++it) { + if ((*it)->senderId() == lastSender) { + fullEventRefresh(it - timelineBottom); + } + } +} + +void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool isPending) +{ + if (event == nullptr) { + return; + } + + // We only create the poll handler for event acknowledged by the server as we need + // an ID + if (!event->id().isEmpty() && event->is()) { + m_room->createPollHandler(eventCast(event)); + } + + auto eventId = event->id(); + auto senderId = event->senderId(); + if (eventId.isEmpty()) { + eventId = event->transactionId(); + } + // 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_memberObjects.contains(senderId)) { + m_memberObjects[senderId] = std::unique_ptr(new NeochatRoomMember(m_room, senderId)); + } + + if (!m_contentModels.contains(eventId) && !m_contentModels.contains(event->transactionId())) { + if (!event->isStateEvent() || event->matrixType() == u"org.matrix.msc3672.beacon_info"_s) { + m_contentModels[eventId] = std::unique_ptr(new MessageContentModel(m_room, eventId, false, isPending)); + } + } + + const auto roomMessageEvent = eventCast(event); + if (roomMessageEvent && roomMessageEvent->isThreaded() && !m_threadModels.contains(roomMessageEvent->threadRootEventId())) { + m_threadModels[roomMessageEvent->threadRootEventId()] = QSharedPointer(new ThreadModel(roomMessageEvent->threadRootEventId(), m_room)); + } + + // ReadMarkerModel handles updates to add and remove markers, we only need to + // handle adding and removing whole models here. + if (m_readMarkerModels.contains(eventId)) { + // If a model already exists but now has no reactions remove it + if (m_readMarkerModels[eventId]->rowCount() <= 0) { + m_readMarkerModels.remove(eventId); + if (!resetting) { + refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole}); + } + } + } else { + auto memberIds = m_room->userIdsAtEvent(eventId); + memberIds.remove(m_room->localMember().id()); + if (memberIds.size() > 0) { + // If a model doesn't exist and there are reactions add it. + auto newModel = QSharedPointer(new ReadMarkerModel(eventId, m_room)); + if (newModel->rowCount() > 0) { + m_readMarkerModels[eventId] = newModel; + if (!resetting) { + refreshEventRoles(eventId, {ReadMarkersRole, ShowReadMarkersRole}); + } + } + } + } + + if (const auto roomEvent = eventCast(event)) { + // ReactionModel handles updates to add and remove reactions, we only need to + // handle adding and removing whole models here. + if (m_reactionModels.contains(eventId)) { + // If a model already exists but now has no reactions remove it + if (m_reactionModels[eventId]->rowCount() <= 0) { + m_reactionModels.remove(eventId); + if (!resetting) { + refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); + } + } + } else { + if (m_room->relatedEvents(*event, Quotient::EventRelation::AnnotationType).count() > 0) { + // If a model doesn't exist and there are reactions add it. + auto reactionModel = QSharedPointer(new ReactionModel(roomEvent, m_room)); + if (reactionModel->rowCount() > 0) { + m_reactionModels[eventId] = reactionModel; + if (!resetting) { + refreshEventRoles(eventId, {ReactionRole, ShowReactionsRole}); + } + } + } + } + } +} + +void MessageModel::clearModel() +{ + if (m_room) { + // HACK: Reset the model to a null room first to make sure QML dismantles + // last room's objects before the room is actually changed + beginResetModel(); + m_room->disconnect(this); + m_room = nullptr; + endResetModel(); + } + + // Don't clear the member objects until the model has been fully reset and all + // refs cleared. + clearEventObjects(); +} + +void MessageModel::clearEventObjects() +{ + m_memberObjects.clear(); + m_contentModels.clear(); + m_reactionModels.clear(); + m_readMarkerModels.clear(); +} + +bool MessageModel::event(QEvent *event) +{ + if (event->type() == QEvent::ApplicationPaletteChange) { + Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole}); + } + return QObject::event(event); +} + +ThreadModel *MessageModel::threadModelForRootId(const QString &threadRootId) const +{ + return m_threadModels[threadRootId].data(); +} + +#include "moc_messagemodel.cpp" diff --git a/src/models/messageeventmodel.h b/src/models/messagemodel.h similarity index 79% rename from src/models/messageeventmodel.h rename to src/models/messagemodel.h index af9e39d70..e50da63bd 100644 --- a/src/models/messageeventmodel.h +++ b/src/models/messagemodel.h @@ -1,12 +1,12 @@ -// SPDX-FileCopyrightText: 2018-2019 Black Hat -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #pragma once #include #include +#include -#include "linkpreviewer.h" #include "messagecontentmodel.h" #include "neochatroom.h" #include "neochatroommember.h" @@ -17,17 +17,17 @@ class ReactionModel; /** - * @class MessageEventModel + * @class MessageModel * - * This class defines the model for visualising the room timeline. + * This class defines a model for visualising the room events. * - * This model covers all event types in the timeline with many of the roles being + * This model covers all event types in the room with many of the roles being * specific to a subset of events. This means the user needs to understand which * roles will return useful information for a given event type. * * @sa NeoChatRoom */ -class MessageEventModel : public QAbstractListModel +class MessageModel : public QAbstractListModel { Q_OBJECT QML_ELEMENT @@ -74,7 +74,7 @@ public: }; Q_ENUM(EventRoles) - explicit MessageEventModel(QObject *parent = nullptr); + explicit MessageModel(QObject *parent = nullptr); [[nodiscard]] NeoChatRoom *room() const; void setRoom(NeoChatRoom *room); @@ -86,13 +86,6 @@ public: */ [[nodiscard]] QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; - /** - * @brief Number of rows in the model. - * - * @sa QAbstractItemModel::rowCount - */ - [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; - /** * @brief Returns a mapping from Role enum values to role names. * @@ -107,14 +100,28 @@ public: Q_INVOKABLE ThreadModel *threadModelForRootId(const QString &threadRootId) const; +Q_SIGNALS: + void roomChanged(); + void newEventAdded(const Quotient::RoomEvent *event, bool isPending = false); + protected: + QPointer m_room; + QPersistentModelIndex m_lastReadEventIndex; + + virtual int timelineServerIndex() const; + virtual std::optional> getEventForIndex(QModelIndex index) const; + + void fullEventRefresh(int row); + int refreshEventRoles(const QString &eventId, const QList &roles = {}); + void refreshEventRoles(int row, const QList &roles = {}); + void refreshLastUserEvents(int baseTimelineRow); + + void clearModel(); + void clearEventObjects(); + bool event(QEvent *event) override; private: - QPointer m_currentRoom = nullptr; - QString lastReadEventId; - QPersistentModelIndex m_lastReadEventIndex; - int rowBelowInserted = -1; bool resetting = false; bool movingEvent = false; @@ -124,21 +131,5 @@ private: QMap> m_threadModels; QMap> m_reactionModels; - [[nodiscard]] int timelineBaseIndex() const; - - bool canFetchMore(const QModelIndex &parent) const override; - void fetchMore(const QModelIndex &parent) override; - - void fullEventRefresh(int row); - void refreshLastUserEvents(int baseTimelineRow); - void refreshEventRoles(int row, const QList &roles = {}); - int refreshEventRoles(const QString &eventId, const QList &roles = {}); - void moveReadMarker(const QString &toEventId); - void createEventObjects(const Quotient::RoomEvent *event, bool isPending = false); - // Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows - bool m_initialized = false; - -Q_SIGNALS: - void roomChanged(); }; diff --git a/src/models/searchmodel.cpp b/src/models/searchmodel.cpp index 90d1c8908..47f0376c5 100644 --- a/src/models/searchmodel.cpp +++ b/src/models/searchmodel.cpp @@ -3,23 +3,12 @@ #include "searchmodel.h" -#include "enums/delegatetype.h" -#include "eventhandler.h" -#include "models/messagecontentmodel.h" -#include "neochatroom.h" - -#include - -#include - -#include - using namespace Quotient; // TODO search only in the current room SearchModel::SearchModel(QObject *parent) - : QAbstractListModel(parent) + : MessageModel(parent) { } @@ -65,15 +54,14 @@ void SearchModel::search() auto job = m_room->connection()->callApi(SearchJob::Categories{criteria}); m_job = job; connect(job, &BaseJob::finished, this, [this, job] { + clearEventObjects(); + beginResetModel(); - m_memberObjects.clear(); m_result = job->searchCategories().roomEvents; if (m_result.has_value()) { for (const auto &result : m_result.value().results) { - if (!m_memberObjects.contains(result.result->senderId())) { - m_memberObjects[result.result->senderId()] = std::unique_ptr(new NeochatRoomMember(m_room, result.result->senderId())); - } + Q_EMIT newEventAdded(result.result.get()); } } @@ -84,57 +72,13 @@ void SearchModel::search() }); } -QVariant SearchModel::data(const QModelIndex &index, int role) const +std::optional> SearchModel::getEventForIndex(QModelIndex index) const { - auto row = index.row(); - const auto &event = *m_result->results[row].result; + if (!m_result.has_value()) { + return std::nullopt; + } - switch (role) { - case AuthorRole: - return QVariant::fromValue(m_memberObjects.at(event.senderId()).get()); - case ShowSectionRole: - if (row == 0) { - return true; - } - return event.originTimestamp().date() != m_result->results[row - 1].result->originTimestamp().date(); - case SectionRole: - return EventHandler::timeString(&event, true); - case ShowReactionsRole: - return false; - case ShowReadMarkersRole: - return false; - case IsPendingRole: - return false; - case HighlightRole: - return EventHandler::isHighlighted(m_room, &event); - case EventIdRole: - return event.displayId(); - case IsThreadedRole: - if (auto roomMessageEvent = eventCast(&event)) { - return roomMessageEvent->isThreaded(); - } - return {}; - case ThreadRootRole: - if (auto roomMessageEvent = eventCast(&event); roomMessageEvent->isThreaded()) { - return roomMessageEvent->threadRootEventId(); - } - return {}; - case ContentModelRole: { - if (!event.isStateEvent()) { - return QVariant::fromValue(new MessageContentModel(m_room, event.id())); - } - if (event.isStateEvent()) { - if (event.matrixType() == u"org.matrix.msc3672.beacon_info"_s) { - return QVariant::fromValue(new MessageContentModel(m_room, event.id())); - } - } - return {}; - } - case IsEditableRole: { - return false; - } - } - return DelegateType::Message; + return *m_result.value().results.at(index.row()).result.get(); } int SearchModel::rowCount(const QModelIndex &parent) const @@ -146,57 +90,11 @@ int SearchModel::rowCount(const QModelIndex &parent) const return 0; } -QHash SearchModel::roleNames() const -{ - return { - {DelegateTypeRole, "delegateType"}, - {AuthorRole, "author"}, - {ShowSectionRole, "showSection"}, - {SectionRole, "section"}, - {EventIdRole, "eventId"}, - {ExcessReadMarkersRole, "excessReadMarkers"}, - {HighlightRole, "isHighlighted"}, - {ReadMarkersString, "readMarkersString"}, - {VerifiedRole, "verified"}, - {ShowReactionsRole, "showReactions"}, - {ReactionRole, "reaction"}, - {ReadMarkersRole, "readMarkers"}, - {IsPendingRole, "isPending"}, - {ShowReadMarkersRole, "showReadMarkers"}, - {IsThreadedRole, "isThreaded"}, - {ThreadRootRole, "threadRoot"}, - {ContentModelRole, "contentModel"}, - {IsEditableRole, "isEditable"}, - }; -} - -NeoChatRoom *SearchModel::room() const -{ - return m_room; -} - -void SearchModel::setRoom(NeoChatRoom *room) -{ - if (m_room) { - disconnect(m_room, nullptr, this, nullptr); - } - m_room = room; - Q_EMIT roomChanged(); -} - bool SearchModel::searching() const { return m_searching; } -bool SearchModel::event(QEvent *event) -{ - if (event->type() == QEvent::ApplicationPaletteChange) { - Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0), {AuthorRole, ReadMarkersRole}); - } - return QObject::event(event); -} - void SearchModel::setSearching(bool searching) { m_searching = searching; diff --git a/src/models/searchmodel.h b/src/models/searchmodel.h index f1d5944a6..669a82737 100644 --- a/src/models/searchmodel.h +++ b/src/models/searchmodel.h @@ -5,11 +5,10 @@ #include #include -#include #include -#include "neochatroommember.h" +#include "messagemodel.h" namespace Quotient { @@ -23,7 +22,7 @@ class NeoChatRoom; * * This class defines the model for visualising the results of a room message search. */ -class SearchModel : public QAbstractListModel +class SearchModel : public MessageModel { Q_OBJECT QML_ELEMENT @@ -33,63 +32,19 @@ class SearchModel : public QAbstractListModel */ Q_PROPERTY(QString searchText READ searchText WRITE setSearchText NOTIFY searchTextChanged) - /** - * @brief The current room that the search is being done from. - */ - Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged) - /** * @brief Whether the model is currently searching for messages. */ Q_PROPERTY(bool searching READ searching NOTIFY searchingChanged) public: - /** - * @brief Defines the model roles. - * - * For documentation of the roles, see MessageEventModel. - * - * Some of the roles exist only for compatibility with the MessageEventModel, - * since the same delegates are used. - */ - enum Roles { - DelegateTypeRole = Qt::DisplayRole + 1, - AuthorRole, - ShowSectionRole, - SectionRole, - EventIdRole, - ExcessReadMarkersRole, - HighlightRole, - ReadMarkersString, - VerifiedRole, - ShowReactionsRole, - ReactionRole, - ReadMarkersRole, - IsPendingRole, - ShowReadMarkersRole, - IsThreadedRole, - ThreadRootRole, - ContentModelRole, - IsEditableRole, - }; - Q_ENUM(Roles) explicit SearchModel(QObject *parent = nullptr); QString searchText() const; void setSearchText(const QString &searchText); - NeoChatRoom *room() const; - void setRoom(NeoChatRoom *room); - bool searching() const; - /** - * @brief Get the given role value at the given index. - * - * @sa QAbstractItemModel::data - */ - QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; - /** * @brief Number of rows in the model. * @@ -97,13 +52,6 @@ public: */ int rowCount(const QModelIndex &parent = QModelIndex()) const override; - /** - * @brief Returns a mapping from Role enum values to role names. - * - * @sa Roles, QAbstractItemModel::roleNames() - */ - QHash roleNames() const override; - /** * @brief Start searching for messages. */ @@ -114,17 +62,13 @@ Q_SIGNALS: void roomChanged(); void searchingChanged(); -protected: - bool event(QEvent *event) override; - private: + std::optional> getEventForIndex(QModelIndex index) const override; + void setSearching(bool searching); QString m_searchText; - QPointer m_room; std::optional m_result = std::nullopt; Quotient::SearchJob *m_job = nullptr; bool m_searching = false; - - std::map> m_memberObjects; }; diff --git a/src/models/timelinemessagemodel.cpp b/src/models/timelinemessagemodel.cpp new file mode 100644 index 000000000..9b6a6b2d3 --- /dev/null +++ b/src/models/timelinemessagemodel.cpp @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: 2018-2019 Black Hat +// SPDX-License-Identifier: GPL-3.0-only + +#include "timelinemessagemodel.h" +#include "messagemodel_logging.h" + +using namespace Quotient; + +TimelineMessageModel::TimelineMessageModel(QObject *parent) + : MessageModel(parent) +{ + connect(this, &TimelineMessageModel::roomChanged, this, &TimelineMessageModel::connectNewRoom); +} + +void TimelineMessageModel::connectNewRoom() +{ + if (m_room) { + m_lastReadEventIndex = QPersistentModelIndex(QModelIndex()); + m_room->setDisplayed(); + + for (auto event = m_room->messageEvents().begin(); event != m_room->messageEvents().end(); ++event) { + Q_EMIT newEventAdded(event->get()); + } + + if (m_room->timelineSize() < 10 && !m_room->allHistoryLoaded()) { + m_room->getPreviousContent(50); + } + + connect(m_room, &Room::aboutToAddNewMessages, this, [this](RoomEventsRange events) { + for (auto &&event : events) { + Q_EMIT newEventAdded(event.get()); + } + m_initialized = true; + beginInsertRows({}, timelineServerIndex(), timelineServerIndex() + int(events.size()) - 1); + }); + connect(m_room, &Room::aboutToAddHistoricalMessages, this, [this](RoomEventsRange events) { + for (auto &event : events) { + Q_EMIT newEventAdded(event.get()); + } + if (rowCount() > 0) { + rowBelowInserted = rowCount() - 1; // See #312 + } + m_initialized = true; + beginInsertRows({}, rowCount(), rowCount() + int(events.size()) - 1); + }); + connect(m_room, &Room::addedMessages, this, [this](int lowest, int biggest) { + if (m_initialized) { + endInsertRows(); + } + if (!m_lastReadEventIndex.isValid()) { + // no read marker, so see if we need to create one. + moveReadMarker(m_room->lastFullyReadEventId()); + } + if (biggest < m_room->maxTimelineIndex()) { + auto rowBelowInserted = m_room->maxTimelineIndex() - biggest + timelineServerIndex() - 1; + refreshEventRoles(rowBelowInserted, {ContentModelRole}); + } + for (auto i = m_room->maxTimelineIndex() - biggest; i <= m_room->maxTimelineIndex() - lowest; ++i) { + refreshLastUserEvents(i); + } + }); +#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 0) + connect(m_room, &Room::pendingEventAdded, this, [this](const Quotient::RoomEvent *event) { + m_initialized = true; + Q_EMIT newEventAdded(event, true); + beginInsertRows({}, 0, 0); + endInsertRows(); + }); +#else + connect(m_room, &Room::pendingEventAboutToAdd, this, [this](Quotient::RoomEvent *event) { + m_initialized = true; + Q_EMIT newEventAdded(event, true); + beginInsertRows({}, 0, 0); + }); + connect(m_room, &Room::pendingEventAdded, this, &TimelineMessageModel::endInsertRows); +#endif + connect(m_room, &Room::pendingEventAboutToMerge, this, [this](RoomEvent *, int i) { + Q_EMIT dataChanged(index(i, 0), index(i, 0), {IsPendingRole}); + if (i == 0) { + return; // No need to move anything, just refresh + } + + movingEvent = true; + // Reverse i because row 0 is bottommost in the model + const auto row = timelineServerIndex() - i - 1; + beginMoveRows({}, row, row, {}, timelineServerIndex()); + }); + connect(m_room, &Room::pendingEventMerged, this, [this] { + if (movingEvent) { + endMoveRows(); + movingEvent = false; + } + fullEventRefresh(timelineServerIndex()); + refreshLastUserEvents(0); + if (timelineServerIndex() > 0) { // Refresh below, see #312 + refreshEventRoles(timelineServerIndex() - 1, {ContentModelRole}); + } + }); + connect(m_room, &Room::pendingEventChanged, this, &TimelineMessageModel::fullEventRefresh); + connect(m_room, &Room::pendingEventAboutToDiscard, this, [this](int i) { + beginRemoveRows({}, i, i); + }); + connect(m_room, &Room::pendingEventDiscarded, this, &TimelineMessageModel::endRemoveRows); + connect(m_room, &Room::fullyReadMarkerMoved, this, [this](const QString &fromEventId, const QString &toEventId) { + Q_UNUSED(fromEventId); + moveReadMarker(toEventId); + }); + connect(m_room, &Room::replacedEvent, this, [this](const RoomEvent *newEvent) { + Q_EMIT newEventAdded(newEvent); + }); + connect(m_room, &Room::updatedEvent, this, [this](const QString &eventId) { + if (eventId.isEmpty()) { // How did we get here? + return; + } + const auto eventIt = m_room->findInTimeline(eventId); + if (eventIt != m_room->historyEdge()) { + Q_EMIT newEventAdded(eventIt->event()); + if (eventIt->event()->is()) { + m_room->createPollHandler(eventCast(eventIt->event())); + } + } + refreshEventRoles(eventId, {Qt::DisplayRole}); + }); + connect(m_room, &Room::changed, this, [this](Room::Changes changes) { + if (changes.testFlag(Quotient::Room::Change::Other)) { + // this is slow + for (auto it = m_room->messageEvents().rbegin(); it != m_room->messageEvents().rend(); ++it) { + Q_EMIT newEventAdded(it->event()); + } + } + }); + connect(m_room->connection(), &Connection::ignoredUsersListChanged, this, [this] { + beginResetModel(); + endResetModel(); + }); + + qCDebug(Message) << "Connected to room" << m_room->id() << "as" << m_room->localMember().id(); + } + + // After reset put a read marker in if required. + // This is needed when changing back to a room that has already loaded messages. + if (m_room) { + moveReadMarker(m_room->lastFullyReadEventId()); + } +} + +int TimelineMessageModel::timelineServerIndex() const +{ + return m_room ? int(m_room->pendingEvents().size()) : 0; +} + +void TimelineMessageModel::moveReadMarker(const QString &toEventId) +{ + const auto timelineIt = m_room->findInTimeline(toEventId); + if (timelineIt == m_room->historyEdge()) { + return; + } + int newRow = int(timelineIt - m_room->messageEvents().rbegin()) + timelineServerIndex(); + + if (!m_lastReadEventIndex.isValid()) { + // Not valid index means we don't display any marker yet, in this case + // we create the new index and insert the row in case the read marker + // need to be displayed. + if (newRow > timelineServerIndex()) { + // The user didn't read all the messages yet. + m_initialized = true; + beginInsertRows({}, newRow, newRow); + m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0)); + endInsertRows(); + return; + } + // The user read all the messages and we didn't display any read marker yet + // => do nothing + return; + } + if (newRow <= timelineServerIndex()) { + // The user read all the messages => remove read marker + beginRemoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row()); + m_lastReadEventIndex = QModelIndex(); + endRemoveRows(); + return; + } + + // The user didn't read all the messages yet but moved the reader marker. + beginMoveRows({}, m_lastReadEventIndex.row(), m_lastReadEventIndex.row(), {}, newRow); + m_lastReadEventIndex = QPersistentModelIndex(index(newRow, 0)); + endMoveRows(); +} + +std::optional> TimelineMessageModel::getEventForIndex(QModelIndex index) const +{ + const auto row = index.row(); + bool isPending = row < timelineServerIndex(); + const auto timelineIt = m_room->messageEvents().crbegin() + + std::max(0, row - timelineServerIndex() - (m_lastReadEventIndex.isValid() && m_lastReadEventIndex.row() < row ? 1 : 0)); + const auto pendingIt = m_room->pendingEvents().crbegin() + std::min(row, timelineServerIndex()); + return isPending ? **pendingIt : **timelineIt; +} + +int TimelineMessageModel::rowCount(const QModelIndex &parent) const +{ + if (!m_room || parent.isValid()) { + return 0; + } + + return int(m_room->pendingEvents().size()) + m_room->timelineSize() + (m_lastReadEventIndex.isValid() ? 1 : 0); +} + +bool TimelineMessageModel::canFetchMore(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + + return m_room && !m_room->eventsHistoryJob() && !m_room->allHistoryLoaded(); +} + +void TimelineMessageModel::fetchMore(const QModelIndex &parent) +{ + Q_UNUSED(parent); + if (m_room) { + m_room->getPreviousContent(20); + } +} + +#include "moc_timelinemessagemodel.cpp" diff --git a/src/models/timelinemessagemodel.h b/src/models/timelinemessagemodel.h new file mode 100644 index 000000000..c62532088 --- /dev/null +++ b/src/models/timelinemessagemodel.h @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2018-2019 Black Hat +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include +#include + +#include "messagemodel.h" + +class ReactionModel; + +namespace Quotient +{ +class RoomEvent; +} + +/** + * @class TimelineMessageModel + * + * This class defines the model for visualising the room timeline. + * + * This model covers all event types in the timeline with many of the roles being + * specific to a subset of events. This means the user needs to understand which + * roles will return useful information for a given event type. + * + * @sa NeoChatRoom + */ +class TimelineMessageModel : public MessageModel +{ + Q_OBJECT + QML_ELEMENT + +public: + /** + * @brief Defines the model roles. + */ + enum EventRoles { + DelegateTypeRole = Qt::UserRole + 1, /**< The delegate type of the message. */ + EventIdRole, /**< The matrix event ID of the event. */ + TimeRole, /**< The timestamp for when the event was sent (as a QDateTime). */ + SectionRole, /**< The date of the event as a string. */ + AuthorRole, /**< The author of the event. */ + HighlightRole, /**< Whether the event should be highlighted. */ + SpecialMarksRole, /**< Whether the event is hidden or not. */ + ProgressInfoRole, /**< Progress info when downloading files. */ + GenericDisplayRole, /**< A generic string based upon the message type. */ + MediaInfoRole, /**< The media info for the event. */ + + ContentModelRole, /**< The MessageContentModel for the event. */ + + 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. */ + + ReadMarkersRole, /**< The first 5 other users at the event for read marker tracking. */ + ShowReadMarkersRole, /**< Whether there are any other user read markers to be shown. */ + ReactionRole, /**< List model for this event. */ + ShowReactionsRole, /**< Whether there are any reactions to be shown. */ + + VerifiedRole, /**< Whether an encrypted message is sent in a verified session. */ + AuthorDisplayNameRole, /**< The displayname for the event's sender; for name change events, the old displayname. */ + IsRedactedRole, /**< Whether an event has been deleted. */ + IsPendingRole, /**< Whether an event is waiting to be accepted by the server. */ + IsEditableRole, /**< Whether the event can be edited by the user. */ + LastRole, // Keep this last + }; + Q_ENUM(EventRoles) + + explicit TimelineMessageModel(QObject *parent = nullptr); + + /** + * @brief Number of rows in the model. + * + * @sa QAbstractItemModel::rowCount + */ + [[nodiscard]] int rowCount(const QModelIndex &parent = QModelIndex()) const override; + +private: + void connectNewRoom(); + + std::optional> getEventForIndex(QModelIndex index) const override; + + int rowBelowInserted = -1; + bool resetting = false; + bool movingEvent = false; + + int timelineServerIndex() const override; + + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + + void moveReadMarker(const QString &toEventId); + + // Hack to ensure that we don't call endInsertRows when we haven't called beginInsertRows + bool m_initialized = false; +}; diff --git a/src/models/timelinemodel.cpp b/src/models/timelinemodel.cpp index 6af8bf367..1f43545d3 100644 --- a/src/models/timelinemodel.cpp +++ b/src/models/timelinemodel.cpp @@ -12,33 +12,33 @@ TimelineModel::TimelineModel(QObject *parent) { m_timelineBeginningModel = new TimelineBeginningModel(this); addSourceModel(m_timelineBeginningModel); - m_messageEventModel = new MessageEventModel(this); - addSourceModel(m_messageEventModel); + m_timelineMessageModel = new TimelineMessageModel(this); + addSourceModel(m_timelineMessageModel); m_timelineEndModel = new TimelineEndModel(this); addSourceModel(m_timelineEndModel); } NeoChatRoom *TimelineModel::room() const { - return m_messageEventModel->room(); + return m_timelineMessageModel->room(); } void TimelineModel::setRoom(NeoChatRoom *room) { // Both models do their own null checking so just pass along. - m_messageEventModel->setRoom(room); + m_timelineMessageModel->setRoom(room); m_timelineBeginningModel->setRoom(room); m_timelineEndModel->setRoom(room); } -MessageEventModel *TimelineModel::messageEventModel() const +TimelineMessageModel *TimelineModel::timelineMessageModel() const { - return m_messageEventModel; + return m_timelineMessageModel; } QHash TimelineModel::roleNames() const { - return m_messageEventModel->roleNames(); + return m_timelineMessageModel->roleNames(); } TimelineBeginningModel::TimelineBeginningModel(QObject *parent) diff --git a/src/models/timelinemodel.h b/src/models/timelinemodel.h index 23defeb66..858902b72 100644 --- a/src/models/timelinemodel.h +++ b/src/models/timelinemodel.h @@ -7,8 +7,8 @@ #include #include -#include "messageeventmodel.h" #include "neochatroom.h" +#include "timelinemessagemodel.h" /** * @class TimelineBeginningModel @@ -25,7 +25,7 @@ public: * @brief Defines the model roles. */ enum Roles { - DelegateTypeRole = MessageEventModel::DelegateTypeRole, /**< The delegate type of the message. */ + DelegateTypeRole = TimelineMessageModel::DelegateTypeRole, /**< The delegate type of the message. */ }; Q_ENUM(Roles) @@ -79,7 +79,7 @@ public: * @brief Defines the model roles. */ enum Roles { - DelegateTypeRole = MessageEventModel::DelegateTypeRole, /**< The delegate type of the message. */ + DelegateTypeRole = TimelineMessageModel::DelegateTypeRole, /**< The delegate type of the message. */ }; Q_ENUM(Roles) @@ -120,9 +120,9 @@ private: * * A model to visualise a room timeline. * - * This model combines a MessageEventModel with a TimelineEndModel. + * This model combines a TimelineMessageModel with a TimelineEndModel. * - * @sa MessageEventModel, TimelineEndModel + * @sa TimelineMessageModel, TimelineEndModel */ class TimelineModel : public QConcatenateTablesProxyModel { @@ -135,9 +135,9 @@ class TimelineModel : public QConcatenateTablesProxyModel Q_PROPERTY(NeoChatRoom *room READ room WRITE setRoom NOTIFY roomChanged) /** - * @brief The MessageEventModel for the timeline. + * @brief The TimelineMessageModel for the timeline. */ - Q_PROPERTY(MessageEventModel *messageEventModel READ messageEventModel CONSTANT) + Q_PROPERTY(TimelineMessageModel *timelineMessageModel READ timelineMessageModel CONSTANT) public: TimelineModel(QObject *parent = nullptr); @@ -145,7 +145,7 @@ public: [[nodiscard]] NeoChatRoom *room() const; void setRoom(NeoChatRoom *room); - MessageEventModel *messageEventModel() const; + TimelineMessageModel *timelineMessageModel() const; /** * @brief Returns a mapping from Role enum values to role names. @@ -158,7 +158,7 @@ Q_SIGNALS: void roomChanged(); private: - MessageEventModel *m_messageEventModel = nullptr; + TimelineMessageModel *m_timelineMessageModel = nullptr; TimelineBeginningModel *m_timelineBeginningModel = nullptr; TimelineEndModel *m_timelineEndModel = nullptr; }; diff --git a/src/qml/NeochatMaximizeComponent.qml b/src/qml/NeochatMaximizeComponent.qml index 5e2cba71b..a664134a7 100644 --- a/src/qml/NeochatMaximizeComponent.qml +++ b/src/qml/NeochatMaximizeComponent.qml @@ -21,13 +21,13 @@ Components.AlbumMaximizeComponent { */ required property NeoChatRoom currentRoom - readonly property string currentEventId: model.data(model.index(content.currentIndex, 0), MessageEventModel.EventIdRole) + readonly property string currentEventId: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.EventIdRole) - readonly property var currentAuthor: model.data(model.index(content.currentIndex, 0), MessageEventModel.AuthorRole) + readonly property var currentAuthor: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.AuthorRole) - readonly property var currentTime: model.data(model.index(content.currentIndex, 0), MessageEventModel.TimeRole) + readonly property var currentTime: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.TimeRole) - readonly property var currentProgressInfo: model.data(model.index(content.currentIndex, 0), MessageEventModel.ProgressInfoRole) + readonly property var currentProgressInfo: model.data(model.index(content.currentIndex, 0), TimelineMessageModel.ProgressInfoRole) /** * @brief Whether the delegate is part of a thread timeline. diff --git a/src/qml/TimelineView.qml b/src/qml/TimelineView.qml index 56116ac92..9b14e5bba 100644 --- a/src/qml/TimelineView.qml +++ b/src/qml/TimelineView.qml @@ -80,16 +80,16 @@ QQC2.ScrollView { running: messageListView.atYBeginning triggeredOnStart: true onTriggered: { - if (messageListView.atYBeginning && root.timelineModel.messageEventModel.canFetchMore(root.timelineModel.index(0, 0))) { - root.timelineModel.messageEventModel.fetchMore(root.timelineModel.index(0, 0)); + if (messageListView.atYBeginning && root.timelineModel.timelineMessageModel.canFetchMore(root.timelineModel.index(0, 0))) { + root.timelineModel.timelineMessageModel.fetchMore(root.timelineModel.index(0, 0)); } } repeat: true } // HACK: The view should do this automatically but doesn't. - onAtYBeginningChanged: if (atYBeginning && root.timelineModel.messageEventModel.canFetchMore(root.timelineModel.index(0, 0))) { - root.timelineModel.messageEventModel.fetchMore(root.timelineModel.index(0, 0)); + onAtYBeginningChanged: if (atYBeginning && root.timelineModel.timelineMessageModel.canFetchMore(root.timelineModel.index(0, 0))) { + root.timelineModel.timelineMessageModel.fetchMore(root.timelineModel.index(0, 0)); } Timer { @@ -325,7 +325,7 @@ QQC2.ScrollView { } function eventToIndex(eventID) { - const index = root.timelineModel.messageEventModel.eventIdToRow(eventID); + const index = root.timelineModel.timelineMessageModel.eventIdToRow(eventID); if (index === -1) return -1; return root.messageFilterModel.mapFromSource(root.timelineModel.index(index, 0)).row; diff --git a/src/timeline/MessageDelegate.qml b/src/timeline/MessageDelegate.qml index 0f5adb443..2d11f7b33 100644 --- a/src/timeline/MessageDelegate.qml +++ b/src/timeline/MessageDelegate.qml @@ -297,7 +297,7 @@ TimelineDelegate { // 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.messageEventModel.threadModelForRootId(root.threadRoot); + return RoomManager.timelineModel.timelineMessageModel.threadModelForRootId(root.threadRoot); } else { return root.contentModel; }