Skip to content

Commit

Permalink
Merge pull request quotient-im#341 from quotient-im/kitsune-relations
Browse files Browse the repository at this point in the history
Reactions and edited messages support
  • Loading branch information
KitsuneRal committed Aug 1, 2019
2 parents c81ce9d + 4052716 commit 5b236df
Show file tree
Hide file tree
Showing 13 changed files with 464 additions and 78 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,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
Expand Down
46 changes: 45 additions & 1 deletion examples/qmc-example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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);
Expand All @@ -44,6 +47,7 @@ class QMCTest : public QObject
const Connection::DirectChatsMap& added);
void conclude();
void finalize();
// clang-format on

private:
QScopedPointer<Connection, QScopedPointerDeleteLater> c;
Expand Down Expand Up @@ -228,8 +232,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()
Expand Down
53 changes: 29 additions & 24 deletions lib/converters.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ namespace QMatrixClient
template <typename T>
struct JsonObjectConverter
{
static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod; }
static void fillFrom(const QJsonObject& jo, T& pod) { pod = jo; }
static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod.toJson(); }
static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); }
};

template <typename T>
Expand Down Expand Up @@ -89,14 +89,16 @@ namespace QMatrixClient
return JsonConverter<T>::dump(pod);
}

inline auto toJson(const QJsonObject& jo) { return jo; }

template <typename T>
inline auto fillJson(QJsonObject& json, const T& data)
inline void fillJson(QJsonObject& json, const T& data)
{
JsonObjectConverter<T>::dumpTo(json, data);
}

template <typename T>
inline auto fromJson(const QJsonValue& jv)
inline T fromJson(const QJsonValue& jv)
{
return JsonConverter<T>::load(jv);
}
Expand All @@ -114,8 +116,7 @@ namespace QMatrixClient
template <typename T>
inline void fromJson(const QJsonValue& jv, T& pod)
{
if (!jv.isUndefined())
pod = fromJson<T>(jv);
pod = jv.isUndefined() ? T() : fromJson<T>(jv);
}

template <typename T>
Expand All @@ -124,21 +125,13 @@ namespace QMatrixClient
pod = fromJson<T>(jd);
}

// Unfolds Omittable<>
template <typename T>
inline void fromJson(const QJsonValue& jv, Omittable<T>& pod)
{
if (jv.isUndefined())
pod = none;
else
pod = fromJson<T>(jv);
}

template <typename T>
inline void fillFromJson(const QJsonValue& jv, T& pod)
{
if (jv.isObject())
JsonObjectConverter<T>::fillFrom(jv.toObject(), pod);
else if (!jv.isUndefined())
pod = fromJson<T>(jv);
}

// JsonConverter<> specialisations
Expand Down Expand Up @@ -224,6 +217,21 @@ namespace QMatrixClient
static QVariant load(const QJsonValue& jv);
};

template <typename T>
struct JsonConverter<Omittable<T>>
{
static QJsonValue dump(const Omittable<T>& from)
{
return from.omitted() ? QJsonValue() : toJson(from.value());
}
static Omittable<T> load(const QJsonValue& jv)
{
if (jv.isUndefined())
return none;
return fromJson<T>(jv);
}
};

template <typename VectorT,
typename T = typename VectorT::value_type>
struct JsonArrayConverter
Expand Down Expand Up @@ -384,23 +392,20 @@ namespace QMatrixClient
ForwardedT&& value)
{
if (!value.isEmpty())
AddNode<ValT>::impl(container,
key, std::forward<ForwardedT>(value));
addTo(container, key, std::forward<ForwardedT>(value));
}
};

// This is a special one that unfolds Omittable<>
template <typename ValT, bool Force>
struct AddNode<Omittable<ValT>, Force>
// This one unfolds Omittable<> (also only when Force is false)
template <typename ValT>
struct AddNode<Omittable<ValT>, false>
{
template <typename ContT, typename OmittableT>
static void impl(ContT& container,
const QString& key, const OmittableT& value)
{
if (!value.omitted())
AddNode<ValT>::impl(container, key, value.value());
else if (Force) // Edge case, no value but must put something
AddNode<ValT>::impl(container, key, QString{});
addTo(container, key, value.value());
}
};

Expand Down
1 change: 1 addition & 0 deletions lib/events/event.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ namespace QMatrixClient
// === Standard Matrix key names and basicEventJson() ===

static const auto TypeKey = QStringLiteral("type");
static const auto BodyKey = QStringLiteral("body");
static const auto ContentKey = QStringLiteral("content");
static const auto EventIdKey = QStringLiteral("event_id");
static const auto UnsignedKey = QStringLiteral("unsigned");
Expand Down
44 changes: 44 additions & 0 deletions lib/events/reactionevent.cpp
Original file line number Diff line number Diff line change
@@ -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);
}
78 changes: 78 additions & 0 deletions lib/events/reactionevent.h
Original file line number Diff line number Diff line change
@@ -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 { Reply(), std::move(eventId) };
}
static EventRelation annotate(QString eventId, QString key)
{
return { Annotation(), std::move(eventId), std::move(key) };
}
static EventRelation replace(QString eventId)
{
return { 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
14 changes: 14 additions & 0 deletions lib/events/roomevent.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ QString RoomEvent::senderId() const
return fullJson()["sender"_ls].toString();
}

bool RoomEvent::isReplaced() const
{
return unsignedJson()["m.relations"_ls].toObject().contains("m.replace");
}

QString RoomEvent::replacedBy() const
{
// clang-format off
return unsignedJson()["m.relations"_ls].toObject()
.value("m.replace").toObject()
.value(EventIdKeyL).toString();
// clang-format on
}

QString RoomEvent::redactionReason() const
{
return isRedacted() ? _redactedBecause->reason() : QString{};
Expand Down
2 changes: 2 additions & 0 deletions lib/events/roomevent.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ namespace QMatrixClient
QDateTime timestamp() const;
QString roomId() const;
QString senderId() const;
bool isReplaced() const;
QString replacedBy() const;
bool isRedacted() const { return bool(_redactedBecause); }
const event_ptr_tt<RedactionEvent>& redactedBecause() const
{
Expand Down
Loading

0 comments on commit 5b236df

Please sign in to comment.