From 4c59f10262b3de2e8b808b0839d772b0d70d2057 Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Mon, 23 Mar 2020 16:55:03 +0100
Subject: qmc-example: make tests work again

---
 examples/qmc-example.cpp | 28 ++++++++++++++++------------
 1 file changed, 16 insertions(+), 12 deletions(-)

(limited to 'examples')

diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp
index bd9190b9..2aaaf147 100644
--- a/examples/qmc-example.cpp
+++ b/examples/qmc-example.cpp
@@ -183,7 +183,7 @@ void QMCTest::loadMembers()
     // The dedicated qmc-test room is too small to test
     // lazy-loading-then-full-loading; use #qmatrixclient:matrix.org instead.
     // TODO: #264
-    auto* r = c->room(QStringLiteral("!PCzUtxtOjUySxSelof:matrix.org"));
+    auto* r = c->roomByAlias(QStringLiteral("#qmatrixclient:matrix.org"));
     if (!r)
     {
         cout << "#test:matrix.org is not found in the test user's rooms" << endl;
@@ -344,8 +344,9 @@ void QMCTest::setTopic()
     const auto newTopic = c->generateTxnId();
     targetRoom->setTopic(newTopic); // Sets the state by proper means
     const auto fakeTopic = c->generateTxnId();
-    targetRoom->postJson(RoomTopicEvent::matrixTypeId(), // Fake state event
-                         RoomTopicEvent(fakeTopic).contentJson());
+    auto fakeTxnId =
+        targetRoom->postJson(RoomTopicEvent::matrixTypeId(), // Fake state event
+                             RoomTopicEvent(fakeTopic).contentJson());
 
     connectUntil(targetRoom, &Room::topicChanged, this,
         [this,newTopic,fakeTopic,initialTopic] {
@@ -353,22 +354,25 @@ void QMCTest::setTopic()
             {
                 QMC_CHECK(stateTestName, true);
                 // Don't reset the topic yet if the negative test still runs
-                if (!running.contains(fakeStateTestName))
-                    targetRoom->setTopic(initialTopic);
+                targetRoom->setTopic(initialTopic);
 
                 return true;
             }
             return false;
         });
 
-    connectUntil(targetRoom, &Room::pendingEventAboutToMerge, this,
-        [this,fakeTopic,initialTopic] (const RoomEvent* e, int) {
-            if (e->contentJson().value("topic").toString() != fakeTopic)
-                return false; // Wait on for the right event
+    connectUntil(targetRoom, &Room::pendingEventChanged, this,
+        [this, fakeTxnId](int pendingIdx) {
+            const auto& pendingEvents = targetRoom->pendingEvents();
+            Q_ASSERT(pendingIdx >= 0 && pendingIdx < int(pendingEvents.size()));
 
-            QMC_CHECK(fakeStateTestName, !e->isStateEvent());
-            if (!running.contains(fakeStateTestName))
-                targetRoom->setTopic(initialTopic);
+            const auto& pendingItem = pendingEvents[pendingIdx];
+            if (pendingItem->transactionId() != fakeTxnId
+                || pendingItem.deliveryStatus() <= EventStatus::Departed)
+                return false;
+
+            QMC_CHECK(fakeStateTestName, pendingItem.deliveryStatus()
+                                             == EventStatus::SendingFailed);
             return true;
         });
 }
-- 
cgit v1.2.3


From 16a0a88b3db9e8c3f1c8ff80139b77a31f2da287 Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Wed, 25 Mar 2020 14:34:31 +0100
Subject: Support for receiving m.reaction events

Continuation of the #341 backport.
---
 CMakeLists.txt               |   1 +
 examples/qmc-example.cpp     |  46 +++++++++++++++-
 lib/events/reactionevent.cpp |  44 ++++++++++++++++
 lib/events/reactionevent.h   |  78 +++++++++++++++++++++++++++
 lib/room.cpp                 | 123 ++++++++++++++++++++++++++++++-------------
 lib/room.h                   |  10 ++++
 libqmatrixclient.pri         |   2 +
 7 files changed, 266 insertions(+), 38 deletions(-)
 create mode 100644 lib/events/reactionevent.cpp
 create mode 100644 lib/events/reactionevent.h

(limited to 'examples')

diff --git a/CMakeLists.txt b/CMakeLists.txt
index b2536f1b..8ae97a6c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -98,6 +98,7 @@ set(libqmatrixclient_SRCS
     lib/events/roommemberevent.cpp
     lib/events/typingevent.cpp
     lib/events/receiptevent.cpp
+    lib/events/reactionevent.cpp
     lib/events/callanswerevent.cpp
     lib/events/callcandidatesevent.cpp
     lib/events/callhangupevent.cpp
diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp
index 2aaaf147..64514619 100644
--- a/examples/qmc-example.cpp
+++ b/examples/qmc-example.cpp
@@ -6,6 +6,7 @@
 #include "csapi/joining.h"
 #include "csapi/leaving.h"
 #include "events/simplestateevents.h"
+#include "events/reactionevent.h"
 
 #include <QtCore/QCoreApplication>
 #include <QtCore/QStringBuilder>
@@ -26,12 +27,14 @@ class QMCTest : public QObject
         QMCTest(Connection* conn, QString testRoomName, QString source);
 
     private slots:
+        // clang-format off
         void setupAndRun();
         void onNewRoom(Room* r);
         void run();
         void doTests();
             void loadMembers();
             void sendMessage();
+                void sendReaction(const QString& targetEvtId);
             void sendFile();
                 void checkFileSendingOutcome(const QString& txnId,
                                              const QString& fileName);
@@ -44,6 +47,7 @@ class QMCTest : public QObject
                         const Connection::DirectChatsMap& added);
         void conclude();
         void finalize();
+        // clang-format on
 
     private:
         QScopedPointer<Connection, QScopedPointerDeleteLater> c;
@@ -230,8 +234,48 @@ void QMCTest::sendMessage()
                 is<RoomMessageEvent>(*evt) && !evt->id().isEmpty() &&
                 pendingEvents[size_t(pendingIdx)]->transactionId()
                     == evt->transactionId());
+            sendReaction(evt->id());
             return true;
-        });
+    });
+}
+
+void QMCTest::sendReaction(const QString& targetEvtId)
+{
+    running.push_back("Reaction sending");
+    cout << "Reacting to the newest message in the room" << endl;
+    Q_ASSERT(targetRoom->timelineSize() > 0);
+    const auto key = QStringLiteral("+1");
+    auto txnId = targetRoom->postReaction(targetEvtId, key);
+    if (!validatePendingEvent(txnId)) {
+        cout << "Invalid pending event right after submitting" << endl;
+        QMC_CHECK("Reaction sending", false);
+        return;
+    }
+
+    // TODO: Check that it came back as a reaction event and that it attached to
+    // the right event
+    connectUntil(targetRoom, &Room::updatedEvent, this,
+                 [this, txnId, key,
+                  targetEvtId](const QString& actualTargetEvtId) {
+                     if (actualTargetEvtId != targetEvtId)
+                         return false;
+                     const auto reactions = targetRoom->relatedEvents(
+                         targetEvtId, EventRelation::Annotation());
+                     // It's a test room, assuming no interference there should
+                     // be exactly one reaction
+                     if (reactions.size() != 1) {
+                         QMC_CHECK("Reaction sending", false);
+                     } else {
+                         const auto* evt =
+                             eventCast<const ReactionEvent>(reactions.back());
+                         QMC_CHECK("Reaction sending",
+                                   is<ReactionEvent>(*evt)
+                                       && !evt->id().isEmpty()
+                                       && evt->relation().key == key
+                                       && evt->transactionId() == txnId);
+                     }
+                     return true;
+                 });
 }
 
 void QMCTest::sendFile()
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);
 
diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri
index be568bd2..79f1d50b 100644
--- a/libqmatrixclient.pri
+++ b/libqmatrixclient.pri
@@ -32,6 +32,7 @@ HEADERS += \
     $$SRCPATH/events/roomavatarevent.h \
     $$SRCPATH/events/typingevent.h \
     $$SRCPATH/events/receiptevent.h \
+    $$SRCPATH/events/reactionevent.h \
     $$SRCPATH/events/callanswerevent.h \
     $$SRCPATH/events/callcandidatesevent.h \
     $$SRCPATH/events/callhangupevent.h \
@@ -75,6 +76,7 @@ SOURCES += \
     $$SRCPATH/events/roommessageevent.cpp \
     $$SRCPATH/events/roommemberevent.cpp \
     $$SRCPATH/events/typingevent.cpp \
+    $$SRCPATH/events/reactionevent.cpp \
     $$SRCPATH/events/callanswerevent.cpp \
     $$SRCPATH/events/callcandidatesevent.cpp \
     $$SRCPATH/events/callhangupevent.cpp \
-- 
cgit v1.2.3