From ea18152fad9b2f79ea26aaa6d60e6b700931c273 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Tue, 10 Sep 2024 09:46:16 -0400 Subject: [PATCH] Visually show reactions while the server is processing them Unlike messages, reactions do not have a "pending" state. This problem is more obvious while the server is under heavy load, or your connection is disconnecting often, etc. This creates a pretty terrible UX as you try to add an emoji, and nothing obvious happens. libQuotient gives us the tools to do this, we can take advantage of this. The main missing component is that we depend on a changed RoomMessage event for reaction updates, but in the pending queue the library gives us a ReactionEvent directly. We can process this, and tell ReactionModel to use this while waiting for the message to be updated. I did some code cleanup in ReactionModel while I'm touching it as well. --- src/messagecontent/models/reactionmodel.cpp | 39 ++++++++++++++------- src/messagecontent/models/reactionmodel.h | 10 ++++++ src/timeline/models/messagemodel.cpp | 24 ++++++++++++- src/timeline/models/messagemodel.h | 2 +- 4 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/messagecontent/models/reactionmodel.cpp b/src/messagecontent/models/reactionmodel.cpp index 2152f8dda..99ee6a1c1 100644 --- a/src/messagecontent/models/reactionmodel.cpp +++ b/src/messagecontent/models/reactionmodel.cpp @@ -25,6 +25,7 @@ ReactionModel::ReactionModel(MessageContentModel *parent, const QString &eventId Q_ASSERT(room != nullptr); connect(m_room, &NeoChatRoom::updatedEvent, this, [this](const QString &eventId) { + m_queuedEvents.clear(); if (m_eventId == eventId) { updateReactions(); } @@ -115,36 +116,44 @@ void ReactionModel::updateReactions() m_shortcodes.clear(); const auto &annotations = m_room->relatedEvents(m_eventId, Quotient::EventRelation::AnnotationType); - if (annotations.isEmpty()) { + auto &pendingEvents = m_queuedEvents; + if (annotations.isEmpty() && pendingEvents.empty()) { endResetModel(); return; - }; + } QMap reactions = {}; + + const auto addReaction = [this, &reactions](const Quotient::ReactionEvent *e) { + reactions[e->key()].append(e->senderId()); + if (e->contentJson()[QStringLiteral("shortcode")].toString().length()) { + m_shortcodes[e->key()] = e->contentJson()[QStringLiteral("shortcode")].toString().toHtmlEscaped(); + } + }; + for (const auto &a : annotations) { if (a->isRedacted()) { // Just in case? continue; } if (const auto &e = eventCast(a)) { - reactions[e->key()].append(e->senderId()); - if (e->contentJson()["shortcode"_L1].toString().length()) { - m_shortcodes[e->key()] = e->contentJson()["shortcode"_L1].toString().toHtmlEscaped(); - } + addReaction(e); } } + for (const auto &e : pendingEvents) { + if (e->isRedacted()) { // Just in case? + continue; + } + addReaction(e); + } + if (reactions.isEmpty()) { endResetModel(); return; } auto i = reactions.constBegin(); while (i != reactions.constEnd()) { - QStringList members; - for (const auto &member : i.value()) { - members.append(member); - } - - m_reactions.append(ReactionModel::Reaction{i.key(), members}); + m_reactions.append(ReactionModel::Reaction{i.key(), i.value()}); ++i; } @@ -161,6 +170,12 @@ QHash ReactionModel::roleNames() const }; } +void ReactionModel::queueReaction(const Quotient::ReactionEvent *event) +{ + m_queuedEvents.push_back(event); + updateReactions(); +} + QString ReactionModel::reactionText(QString text) const { text = text.toHtmlEscaped(); diff --git a/src/messagecontent/models/reactionmodel.h b/src/messagecontent/models/reactionmodel.h index f65a28b87..3eb7cc614 100644 --- a/src/messagecontent/models/reactionmodel.h +++ b/src/messagecontent/models/reactionmodel.h @@ -70,6 +70,15 @@ public: */ [[nodiscard]] QHash roleNames() const override; + /** + * @brief Puts a ReactionEvent into the pending queue. This reaction should be pulled from the pending queue. + * + * This queue is cleared once the message is updated. + * + * @param event The ReactionEvent to add. + */ + void queueReaction(const Quotient::ReactionEvent *event); + Q_SIGNALS: /** * @brief The reactions in the model have been updated. @@ -81,6 +90,7 @@ private: QString m_eventId; QList m_reactions; QMap m_shortcodes; + QList m_queuedEvents; void updateReactions(); QString reactionText(QString text) const; diff --git a/src/timeline/models/messagemodel.cpp b/src/timeline/models/messagemodel.cpp index 8bbf3f002..68d618f47 100644 --- a/src/timeline/models/messagemodel.cpp +++ b/src/timeline/models/messagemodel.cpp @@ -428,7 +428,7 @@ void MessageModel::refreshLastUserEvents(int baseTimelineRow) } } -void MessageModel::createEventObjects(const Quotient::RoomEvent *event) +void MessageModel::createEventObjects(const Quotient::RoomEvent *event, bool pending) { if (event == nullptr) { return; @@ -469,6 +469,28 @@ void MessageModel::createEventObjects(const Quotient::RoomEvent *event) } } } + + if (pending) { + if (const auto reactionEvent = eventCast(event)) { + auto targetEvent = m_currentRoom->getEvent(reactionEvent->eventId()); + if (!targetEvent) { + return; + } + + if (const auto roomEvent = eventCast(targetEvent)) { + if (m_reactionModels.contains(targetEvent->id())) { + m_reactionModels[targetEvent->id()]->queueReaction(reactionEvent); + } else { + auto reactionModel = QSharedPointer(new ReactionModel(roomEvent, m_currentRoom)); + m_reactionModels[targetEvent->id()] = reactionModel; + reactionModel->queueReaction(reactionEvent); + if (!resetting) { + refreshEventRoles(targetEvent->id(), {ReactionRole, ShowReactionsRole}); + } + } + } + } + } } void MessageModel::moveReadMarker(const QString &toEventId) diff --git a/src/timeline/models/messagemodel.h b/src/timeline/models/messagemodel.h index 00fa5c930..b0fe97249 100644 --- a/src/timeline/models/messagemodel.h +++ b/src/timeline/models/messagemodel.h @@ -192,7 +192,7 @@ protected: private: QMap> m_readMarkerModels; - void createEventObjects(const Quotient::RoomEvent *event); + void createEventObjects(const Quotient::RoomEvent *event, bool pending); static std::function m_hiddenFilter; static bool m_threadsEnabled;