Compare commits

...

1 Commits

Author SHA1 Message Date
Joshua Goins
ea18152fad 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.
2025-09-07 10:47:34 +02:00
4 changed files with 61 additions and 14 deletions

View File

@@ -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<QString, QStringList> 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<const Quotient::ReactionEvent>(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<int, QByteArray> 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();

View File

@@ -70,6 +70,15 @@ public:
*/
[[nodiscard]] QHash<int, QByteArray> 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<Reaction> m_reactions;
QMap<QString, QString> m_shortcodes;
QList<const Quotient::ReactionEvent *> m_queuedEvents;
void updateReactions();
QString reactionText(QString text) const;

View File

@@ -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<const ReactionEvent>(event)) {
auto targetEvent = m_currentRoom->getEvent(reactionEvent->eventId());
if (!targetEvent) {
return;
}
if (const auto roomEvent = eventCast<const RoomMessageEvent>(targetEvent)) {
if (m_reactionModels.contains(targetEvent->id())) {
m_reactionModels[targetEvent->id()]->queueReaction(reactionEvent);
} else {
auto reactionModel = QSharedPointer<ReactionModel>(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)

View File

@@ -192,7 +192,7 @@ protected:
private:
QMap<QString, QSharedPointer<ReadMarkerModel>> m_readMarkerModels;
void createEventObjects(const Quotient::RoomEvent *event);
void createEventObjects(const Quotient::RoomEvent *event, bool pending);
static std::function<bool(const Quotient::RoomEvent *)> m_hiddenFilter;
static bool m_threadsEnabled;