aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorKitsune Ral <Kitsune-Ral@users.sf.net>2020-03-25 14:34:31 +0100
committerKitsune Ral <Kitsune-Ral@users.sf.net>2020-03-26 22:52:01 +0100
commit16a0a88b3db9e8c3f1c8ff80139b77a31f2da287 (patch)
treee210b9c7cd0d2cf615d1f1b5cd7f673c027dba4d /lib
parent5b7032e414899b5f9e8f19aec567eaed5e0fc4c2 (diff)
downloadlibquotient-16a0a88b3db9e8c3f1c8ff80139b77a31f2da287.tar.gz
libquotient-16a0a88b3db9e8c3f1c8ff80139b77a31f2da287.zip
Support for receiving m.reaction events
Continuation of the #341 backport.
Diffstat (limited to 'lib')
-rw-r--r--lib/events/reactionevent.cpp44
-rw-r--r--lib/events/reactionevent.h78
-rw-r--r--lib/room.cpp123
-rw-r--r--lib/room.h10
4 files changed, 218 insertions, 37 deletions
diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp
new file mode 100644
index 00000000..0081edc2
--- /dev/null
+++ b/lib/events/reactionevent.cpp
@@ -0,0 +1,44 @@
+/******************************************************************************
+ * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "reactionevent.h"
+
+using namespace QMatrixClient;
+
+void QMatrixClient::JsonObjectConverter<EventRelation>::dumpTo(
+ QJsonObject& jo, const EventRelation& pod)
+{
+ if (pod.type.isEmpty()) {
+ qCWarning(MAIN) << "Empty relation type; won't dump to JSON";
+ return;
+ }
+ jo.insert(QStringLiteral("rel_type"), pod.type);
+ jo.insert(EventIdKey, pod.eventId);
+ if (pod.type == EventRelation::Annotation())
+ jo.insert(QStringLiteral("key"), pod.key);
+}
+
+void QMatrixClient::JsonObjectConverter<EventRelation>::fillFrom(
+ const QJsonObject& jo, EventRelation& pod)
+{
+ // The experimental logic for generic relationships (MSC1849)
+ fromJson(jo["rel_type"_ls], pod.type);
+ fromJson(jo[EventIdKeyL], pod.eventId);
+ if (pod.type == EventRelation::Annotation())
+ fromJson(jo["key"_ls], pod.key);
+}
diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h
new file mode 100644
index 00000000..a422abeb
--- /dev/null
+++ b/lib/events/reactionevent.h
@@ -0,0 +1,78 @@
+/******************************************************************************
+ * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#pragma once
+
+#include "roomevent.h"
+
+namespace QMatrixClient {
+
+struct EventRelation
+{
+ using reltypeid_t = const char*;
+ static constexpr reltypeid_t Reply() { return "m.in_reply_to"; }
+ static constexpr reltypeid_t Annotation() { return "m.annotation"; }
+ static constexpr reltypeid_t Replacement() { return "m.replace"; }
+
+ QString type;
+ QString eventId;
+ QString key = {}; // Only used for m.annotation for now
+
+ static EventRelation replyTo(QString eventId)
+ {
+ return EventRelation { Reply(), std::move(eventId) };
+ }
+ static EventRelation annotate(QString eventId, QString key)
+ {
+ return EventRelation { Annotation(), std::move(eventId), std::move(key) };
+ }
+ static EventRelation replace(QString eventId)
+ {
+ return EventRelation { Replacement(), std::move(eventId) };
+ }
+};
+template <>
+struct JsonObjectConverter<EventRelation>
+{
+ static void dumpTo(QJsonObject& jo, const EventRelation& pod);
+ static void fillFrom(const QJsonObject& jo, EventRelation& pod);
+};
+
+class ReactionEvent : public RoomEvent
+{
+public:
+ DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent)
+
+ explicit ReactionEvent(const EventRelation& value)
+ : RoomEvent(typeId(), matrixTypeId(),
+ { { QStringLiteral("m.relates_to"), toJson(value) } })
+ {}
+ explicit ReactionEvent(const QJsonObject& obj)
+ : RoomEvent(typeId(), obj)
+ {}
+ EventRelation relation() const
+ {
+ return content<EventRelation>(QStringLiteral("m.relates_to"));
+ }
+
+private:
+ EventRelation _relation;
+};
+REGISTER_EVENT_TYPE(ReactionEvent)
+
+} // namespace QMatrixClient
diff --git a/lib/room.cpp b/lib/room.cpp
index ec000519..3cabe948 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -37,6 +37,7 @@
#include "events/roommemberevent.h"
#include "events/typingevent.h"
#include "events/receiptevent.h"
+#include "events/reactionevent.h"
#include "events/callinviteevent.h"
#include "events/callcandidatesevent.h"
#include "events/callanswerevent.h"
@@ -98,6 +99,10 @@ class Room::Private
Timeline timeline;
PendingEvents unsyncedEvents;
QHash<QString, TimelineItem::index_t> eventsIndex;
+ // A map from evtId to a map of relation type to a vector of event
+ // pointers. Not using QMultiHash, because we want to quickly return
+ // a number of relations for a given event without enumerating them.
+ QHash<QPair<QString, QString>, RelatedEvents> relations;
QString displayname;
Avatar avatar;
int highlightCount = 0;
@@ -707,10 +712,10 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const
if (!d->timeline.empty() && d->eventsIndex.contains(evtId))
{
auto it = findInTimeline(d->eventsIndex.value(evtId));
- Q_ASSERT((*it)->id() == evtId);
+ Q_ASSERT(it != historyEdge() && (*it)->id() == evtId);
return it;
}
- return timelineEdge();
+ return historyEdge();
}
Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId)
@@ -726,6 +731,18 @@ Room::findPendingEvent(const QString& txnId) const
[txnId] (const auto& item) { return item->transactionId() == txnId; });
}
+const Room::RelatedEvents Room::relatedEvents(const QString& evtId,
+ const char* relType) const
+{
+ return d->relations.value({ evtId, relType });
+}
+
+const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt,
+ const char* relType) const
+{
+ return relatedEvents(evt.id(), relType);
+}
+
void Room::Private::getAllMembers()
{
// If already loaded or already loading, there's nothing to do here.
@@ -1569,6 +1586,11 @@ QString Room::postHtmlText(const QString& plainText, const QString& html)
return postHtmlMessage(plainText, html);
}
+QString Room::postReaction(const QString &eventId, const QString &key)
+{
+ return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key));
+}
+
QString Room::postFile(const QString& plainText, const QUrl& localPath,
bool asGenericFile)
{
@@ -2032,6 +2054,14 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
updateDisplayname();
}
}
+ if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) {
+ const auto& targetEvtId = reaction->relation().eventId;
+ const auto lookupKey = qMakePair(targetEvtId,
+ EventRelation::Annotation());
+ if (relations.contains(lookupKey)) {
+ relations[lookupKey].removeOne(reaction);
+ }
+ }
q->onRedaction(*oldEvent, *ti);
emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
return true;
@@ -2112,45 +2142,49 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
if (events.empty())
return Change::NoChange;
- // Pre-process redactions so that events that get redacted in the same
- // batch landed in the timeline already redacted.
- // NB: We have to store redaction events to the timeline too - see #220.
- auto it = std::find_if(events.begin(), events.end(), isEditing);
- for(const auto& eptr: RoomEventsRange(it, events.end()))
{
- if (auto* r = eventCast<RedactionEvent>(eptr))
- {
- // Try to find the target in the timeline, then in the batch.
- if (processRedaction(*r))
- continue;
- auto targetIt = std::find_if(events.begin(), it,
- [id=r->redactedEvent()] (const RoomEventPtr& ep) {
- return ep->id() == id;
- });
- if (targetIt != it)
- *targetIt = makeRedacted(**targetIt, *r);
- else
- qCDebug(MAIN) << "Redaction" << r->id()
- << "ignored: target event" << r->redactedEvent()
- << "is not found";
- // If the target event comes later, it comes already redacted.
- }
- if (auto* msg = eventCast<RoomMessageEvent>(eptr)) {
- if (!msg->replacedEvent().isEmpty()) {
- if (processReplacement(*msg))
+ // Pre-process redactions and edits so that events that get
+ // redacted/replaced in the same batch landed in the timeline already
+ // treated.
+ // NB: We have to store redacting/replacing events to the timeline too -
+ // see #220.
+ auto it = std::find_if(events.begin(), events.end(), isEditing);
+ for (const auto& eptr: RoomEventsRange(it, events.end())) {
+ if (auto* r = eventCast<RedactionEvent>(eptr)) {
+ // Try to find the target in the timeline, then in the batch.
+ if (processRedaction(*r))
continue;
auto targetIt = std::find_if(events.begin(), it,
- [id=msg->replacedEvent()] (const RoomEventPtr& ep) {
- return ep->id() == id;
- });
+ [id = r->redactedEvent()](
+ const RoomEventPtr& ep) {
+ return ep->id() == id;
+ });
if (targetIt != it)
- *targetIt = makeReplaced(**targetIt, *msg);
- else // FIXME: don't ignore, just show it wherever it arrived
- qCDebug(MAIN) << "Replacing event" << msg->id()
- << "ignored: replaced event" << msg->replacedEvent()
- << "is not found";
- // Same as with redactions above, the replaced event coming
- // later will come already with the new content.
+ *targetIt = makeRedacted(**targetIt, *r);
+ else
+ qCDebug(MAIN)
+ << "Redaction" << r->id() << "ignored: target event"
+ << r->redactedEvent() << "is not found";
+ // If the target event comes later, it comes already redacted.
+ }
+ if (auto* msg = eventCast<RoomMessageEvent>(eptr)) {
+ if (!msg->replacedEvent().isEmpty()) {
+ if (processReplacement(*msg))
+ continue;
+ auto targetIt = std::find_if(events.begin(), it,
+ [id = msg->replacedEvent()](
+ const RoomEventPtr& ep) {
+ return ep->id() == id;
+ });
+ if (targetIt != it)
+ *targetIt = makeReplaced(**targetIt, *msg);
+ else // FIXME: don't ignore, just show it wherever it arrived
+ qCDebug(MAIN) << "Replacing event" << msg->id()
+ << "ignored: replaced event"
+ << msg->replacedEvent() << "is not found";
+ // Same as with redactions above, the replaced event coming
+ // later will come already with the new content.
+ }
}
}
}
@@ -2219,6 +2253,14 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
if (totalInserted > 0)
{
+ for (auto it = from; it != timeline.cend(); ++it) {
+ if (const auto* reaction = it->viewAs<ReactionEvent>()) {
+ const auto& relation = reaction->relation();
+ relations[{ relation.eventId, relation.type }] << reaction;
+ emit q->updatedEvent(relation.eventId);
+ }
+ }
+
qCDebug(MAIN)
<< "Room" << q->objectName() << "received" << totalInserted
<< "new events; the last event is now" << timeline.back();
@@ -2277,6 +2319,13 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
q->onAddHistoricalTimelineEvents(from);
emit q->addedMessages(timeline.front().index(), from->index());
+ for (auto it = from; it != timeline.crend(); ++it) {
+ if (const auto* reaction = it->viewAs<ReactionEvent>()) {
+ const auto& relation = reaction->relation();
+ relations[{ relation.eventId, relation.type }] << reaction;
+ emit q->updatedEvent(relation.eventId);
+ }
+ }
if (from <= q->readMarker())
updateUnreadCount(from, timeline.crend());
diff --git a/lib/room.h b/lib/room.h
index b5753c79..87ff3b5d 100644
--- a/lib/room.h
+++ b/lib/room.h
@@ -117,6 +117,7 @@ namespace QMatrixClient
public:
using Timeline = std::deque<TimelineItem>;
using PendingEvents = std::vector<PendingEventItem>;
+ using RelatedEvents = QVector<const RoomEvent*>;
using rev_iter_t = Timeline::const_reverse_iterator;
using timeline_iter_t = Timeline::const_iterator;
@@ -248,6 +249,11 @@ namespace QMatrixClient
PendingEvents::iterator findPendingEvent(const QString & txnId);
PendingEvents::const_iterator findPendingEvent(const QString & txnId) const;
+ const RelatedEvents relatedEvents(const QString& evtId,
+ const char* relType) const;
+ const RelatedEvents relatedEvents(const RoomEvent& evt,
+ const char* relType) const;
+
bool displayed() const;
/// Mark the room as currently displayed to the user
/**
@@ -413,6 +419,9 @@ namespace QMatrixClient
const QString& html,
MessageEventType type = MessageEventType::Text);
QString postHtmlText(const QString& plainText, const QString& html);
+ /** Send a reaction on a given event with a given key */
+ QString postReaction(const QString& eventId, const QString& key);
+
QString postFile(const QString& plainText, const QUrl& localPath,
bool asGenericFile = false);
/** Post a pre-created room message event
@@ -559,6 +568,7 @@ namespace QMatrixClient
void tagsAboutToChange();
void tagsChanged();
+ void updatedEvent(QString eventId);
void replacedEvent(const RoomEvent* newEvent,
const RoomEvent* oldEvent);