From dc53839b2afd2e1ee5af882924a1c0a5b553da4e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Mon, 23 Mar 2020 08:56:01 +0100 Subject: Support for server notices rooms (MSC1452) Fixes #326. --- lib/events/accountdataevents.h | 1 + 1 file changed, 1 insertion(+) (limited to 'lib/events') diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index a99d85ac..a43e358c 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -28,6 +28,7 @@ namespace QMatrixClient { constexpr const char* FavouriteTag = "m.favourite"; constexpr const char* LowPriorityTag = "m.lowpriority"; + constexpr const char* ServerNoticeTag = "m.server_notice"; struct TagRecord { -- cgit v1.2.3 From 5b7032e414899b5f9e8f19aec567eaed5e0fc4c2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 24 Mar 2020 22:30:53 +0100 Subject: Support of MSC1849 message editing This is a backport of #341, as fixed by #373 and #376. --- lib/events/roomevent.cpp | 14 ++++++ lib/events/roomevent.h | 2 + lib/events/roommessageevent.cpp | 108 ++++++++++++++++++++++++++++++---------- lib/events/roommessageevent.h | 3 ++ lib/room.cpp | 93 +++++++++++++++++++++++++++++++--- 5 files changed, 187 insertions(+), 33 deletions(-) (limited to 'lib/events') diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 3d03509f..5e2d0b3c 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -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{}; diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index ce96174e..8926ab0f 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -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& redactedBecause() const { diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index 8f4e0ebc..1edf82e4 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -30,12 +30,13 @@ using namespace EventContent; using MsgType = RoomMessageEvent::MsgType; -static const auto RelatesToKey = "m.relates_to"_ls; -static const auto MsgTypeKey = "msgtype"_ls; -static const auto BodyKey = "body"_ls; -static const auto FormattedBodyKey = "formatted_body"_ls; +static const auto RelatesToKeyL = "m.relates_to"_ls; +static const auto MsgTypeKeyL = "msgtype"_ls; +static const auto BodyKeyL = "body"_ls; +static const auto FormattedBodyKeyL = "formatted_body"_ls; static const auto TextTypeKey = "m.text"; +static const auto EmoteTypeKey = "m.emote"; static const auto NoticeTypeKey = "m.notice"; static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); @@ -49,7 +50,7 @@ TypedBase* make(const QJsonObject& json) template <> TypedBase* make(const QJsonObject& json) { - return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) + return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL) ? new TextContent(json) : nullptr; } @@ -62,7 +63,7 @@ struct MsgTypeDesc const std::vector msgTypes = { { TextTypeKey, MsgType::Text, make } - , { QStringLiteral("m.emote"), MsgType::Emote, make } + , { EmoteTypeKey, MsgType::Emote, make } , { NoticeTypeKey, MsgType::Notice, make } , { QStringLiteral("m.image"), MsgType::Image, make } , { QStringLiteral("m.file"), MsgType::File, make } @@ -95,12 +96,27 @@ QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) { auto json = content ? content->toJson() : QJsonObject(); - if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && - json.contains(RelatesToKey)) - { - json.remove(RelatesToKey); - qCWarning(EVENTS) << RelatesToKey << "cannot be used in" << jsonMsgType - << "messages; the relation has been stripped off"; + if (json.contains(RelatesToKeyL)) { + if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey + && jsonMsgType != EmoteTypeKey) { + json.remove(RelatesToKeyL); + qCWarning(EVENTS) + << RelatesToKeyL << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; + } else { + // After the above, we know for sure that the content is TextContent + // and that its RelatesTo structure is not omitted + auto* textContent = static_cast(content); + if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) { + auto newContentJson = json.take("m.new_content"_ls).toObject(); + newContentJson.insert(BodyKeyL, plainBody); + newContentJson.insert(MsgTypeKeyL, jsonMsgType); + json.insert(QStringLiteral("m.new_content"), newContentJson); + json[MsgTypeKeyL] = jsonMsgType; + json[BodyKeyL] = "* " + plainBody; + return json; + } + } } json.insert(QStringLiteral("msgtype"), jsonMsgType); json.insert(QStringLiteral("body"), plainBody); @@ -159,9 +175,9 @@ RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) if (isRedacted()) return; const QJsonObject content = contentJson(); - if ( content.contains(MsgTypeKey) && content.contains(BodyKey) ) + if ( content.contains(MsgTypeKeyL) && content.contains(BodyKeyL) ) { - auto msgtype = content[MsgTypeKey].toString(); + auto msgtype = content[MsgTypeKeyL].toString(); bool msgTypeFound = false; for (const auto& mt: msgTypes) if (mt.matrixType == msgtype) @@ -191,12 +207,12 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const QString RoomMessageEvent::rawMsgtype() const { - return contentJson()[MsgTypeKey].toString(); + return contentJson()[MsgTypeKeyL].toString(); } QString RoomMessageEvent::plainBody() const { - return contentJson()[BodyKey].toString(); + return contentJson()[BodyKeyL].toString(); } QMimeType RoomMessageEvent::mimeType() const @@ -223,6 +239,16 @@ bool RoomMessageEvent::hasThumbnail() const return content() && content()->thumbnailInfo(); } +QString RoomMessageEvent::replacedEvent() const +{ + if (!content() || !hasTextContent()) + return {}; + + const auto& rel = static_cast(content())->relatesTo; + return !rel.omitted() && rel->type == RelatesTo::ReplacementTypeId() + ? rel->eventId : QString(); +} + QString rawMsgTypeForMimeType(const QMimeType& mimeType) { auto name = mimeType.name(); @@ -251,41 +277,69 @@ TextContent::TextContent(const QString& text, const QString& contentType, mimeType = QMimeDatabase().mimeTypeForName("text/html"); } +namespace QMatrixClient +{ +Omittable relationFromJson(const QJsonValue& jv) +{ + const auto jo = jv.toObject(); + if (jo.isEmpty()) + return none; + const auto replyJson = jo.value(RelatesTo::ReplyTypeId()).toObject(); + if (!replyJson.isEmpty()) + return replyTo(fromJson(replyJson[EventIdKeyL])); + + return RelatesTo { jo.value("rel_type"_ls).toString(), + jo.value(EventIdKeyL).toString() }; +} +} + TextContent::TextContent(const QJsonObject& json) + : relatesTo(relationFromJson(json[RelatesToKeyL])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); static const auto HtmlMimeType = db.mimeTypeForName("text/html"); + const auto actualJson = + relatesTo.omitted() || relatesTo->type != RelatesTo::ReplacementTypeId() + ? json : json.value("m.new_content"_ls).toObject(); // Special-casing the custom matrix.org's (actually, Riot's) way // of sending HTML messages. - if (json["format"_ls].toString() == HtmlContentTypeId) + if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = json[FormattedBodyKey].toString(); + body = actualJson[FormattedBodyKeyL].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. mimeType = PlainTextMimeType; - body = json[BodyKey].toString(); + body = actualJson[BodyKeyL].toString(); } - const auto replyJson = json[RelatesToKey].toObject() - .value(RelatesTo::ReplyTypeId()).toObject(); - if (!replyJson.isEmpty()) - relatesTo = replyTo(fromJson(replyJson[EventIdKeyL])); } void TextContent::fillJson(QJsonObject* json) const { + static const auto FormatKey = QStringLiteral("format"); + static const auto RichBodyKey = QStringLiteral("formatted_body"); + Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(QStringLiteral("format"), HtmlContentTypeId); - json->insert(QStringLiteral("formatted_body"), body); + json->insert(FormatKey, HtmlContentTypeId); + json->insert(RichBodyKey, body); } - if (!relatesTo.omitted()) + if (!relatesTo.omitted()) { json->insert(QStringLiteral("m.relates_to"), - QJsonObject { { relatesTo->type, relatesTo->eventId } }); + QJsonObject { { relatesTo->type, relatesTo->eventId } }); + if (relatesTo->type == RelatesTo::ReplacementTypeId()) { + QJsonObject newContentJson; + if (mimeType.inherits("text/html")) { + json->insert(FormatKey, HtmlContentTypeId); + json->insert(RichBodyKey, body); + } + json->insert(QStringLiteral("m.new_content"), newContentJson); + } + } } LocationContent::LocationContent(const QString& geoUri, diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index c2e075eb..7320e4ea 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -72,6 +72,7 @@ namespace QMatrixClient bool hasTextContent() const; bool hasFileContent() const; bool hasThumbnail() const; + QString replacedEvent() const; static QString rawMsgTypeForUrl(const QUrl& url); static QString rawMsgTypeForFile(const QFileInfo& fi); @@ -79,6 +80,7 @@ namespace QMatrixClient private: QScopedPointer _content; + // FIXME: should it really be static? static QJsonObject assembleContentJson(const QString& plainBody, const QString& jsonMsgType, EventContent::TypedBase* content); @@ -95,6 +97,7 @@ namespace QMatrixClient struct RelatesTo { static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } + static constexpr const char* ReplacementTypeId() { return "m.replace"; } QString type; // The only supported relation so far QString eventId; }; diff --git a/lib/room.cpp b/lib/room.cpp index 960d8147..ec000519 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -293,9 +293,18 @@ class Room::Private * * Tries to find an event in the timeline and redact it; deletes the * redaction event whether the redacted event was found or not. + * \return true if the event has been found and redacted; false otherwise */ bool processRedaction(const RedactionEvent& redaction); + /*! Apply a new revision of the event to the timeline + * + * Tries to find an event in the timeline and replace it with the new + * content passed in \p newMessage. + * \return true if the event has been found and replaced; false otherwise + */ + bool processReplacement(const RoomMessageEvent& newMessage); + void setTags(TagsMap newTags); QJsonObject toJson() const; @@ -2028,6 +2037,52 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) return true; } +/** Make a replaced event + * + * Takes \p target and returns a copy of it with content taken from + * \p replacement. Disposal of the original event after that is on the caller. + */ +RoomEventPtr makeReplaced(const RoomEvent& target, + const RoomMessageEvent& replacement) +{ + auto originalJson = target.originalJsonObject(); + originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls); + + auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); + auto relations = unsignedData.take("m.relations"_ls).toObject(); + relations["m.replace"_ls] = replacement.id(); + unsignedData.insert(QStringLiteral("m.relations"), relations); + originalJson.insert(UnsignedKey, unsignedData); + + return loadEvent(originalJson); +} + +bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) +{ + // Can't use findInTimeline because it returns a const iterator, and + // we need to change the underlying TimelineItem. + const auto pIdx = eventsIndex.find(newEvent.replacedEvent()); + if (pIdx == eventsIndex.end()) + return false; + + Q_ASSERT(q->isValidIndex(*pIdx)); + + auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; + if (ti->replacedBy() == newEvent.id()) + { + qCDebug(MAIN) << "Event" << ti->id() << "is already replaced with" + << newEvent.id(); + return true; + } + + // Make a new event from the redacted JSON and put it in the timeline + // instead of the redacted one. oldEvent will be deleted on return. + auto oldEvent = ti.replaceEvent(makeReplaced(*ti, newEvent)); + qCDebug(MAIN) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); + emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); + return true; +} + Connection* Room::connection() const { Q_ASSERT(d->connection); @@ -2039,10 +2094,16 @@ User* Room::localUser() const return connection()->user(); } -inline bool isRedaction(const RoomEventPtr& ep) +/// Whether the event is a redaction or a replacement +inline bool isEditing(const RoomEventPtr& ep) { Q_ASSERT(ep); - return is(*ep); + if (is(*ep)) + return true; + if (auto* msgEvent = eventCast(ep)) + return msgEvent->replacedEvent().isEmpty(); + + return false; } Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) @@ -2054,18 +2115,19 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // 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 redactionIt = std::find_if(events.begin(), events.end(), isRedaction); - for(const auto& eptr: RoomEventsRange(redactionIt, events.end())) + auto it = std::find_if(events.begin(), events.end(), isEditing); + for(const auto& eptr: RoomEventsRange(it, events.end())) + { if (auto* r = eventCast(eptr)) { // Try to find the target in the timeline, then in the batch. if (processRedaction(*r)) continue; - auto targetIt = std::find_if(events.begin(), redactionIt, + auto targetIt = std::find_if(events.begin(), it, [id=r->redactedEvent()] (const RoomEventPtr& ep) { return ep->id() == id; }); - if (targetIt != redactionIt) + if (targetIt != it) *targetIt = makeRedacted(**targetIt, *r); else qCDebug(MAIN) << "Redaction" << r->id() @@ -2073,6 +2135,25 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) << "is not found"; // If the target event comes later, it comes already redacted. } + if (auto* msg = eventCast(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. + } + } + } // State changes arrive as a part of timeline; the current room state gets // updated before merging events to the timeline because that's what -- cgit v1.2.3 From 16a0a88b3db9e8c3f1c8ff80139b77a31f2da287 Mon Sep 17 00:00:00 2001 From: Kitsune Ral 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 'lib/events') 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 #include @@ -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 c; @@ -230,8 +234,48 @@ void QMCTest::sendMessage() is(*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(reactions.back()); + QMC_CHECK("Reaction sending", + is(*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 + * + * 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::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::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 + * + * 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 +{ + 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(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 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, 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(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(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(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(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(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(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()) { + 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()) { + 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; using PendingEvents = std::vector; + using RelatedEvents = QVector; 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 From f5876b7c9fbaf07696605929e37f9921ce6e745e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 27 Mar 2020 18:07:13 +0100 Subject: Fix compatibility with MSVC 2015 --- lib/events/reactionevent.h | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'lib/events') diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index a422abeb..7a4c9b5a 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -22,8 +22,11 @@ namespace QMatrixClient { -struct EventRelation -{ +struct EventRelation { + // To please MSVC 2015 that doesn't handle initialiser lists like proper + EventRelation(QString type = {}, QString eventId = {}, QString key = {}) + : type(std::move(type)), eventId(std::move(eventId)), key(std::move(key)) + {} using reltypeid_t = const char*; static constexpr reltypeid_t Reply() { return "m.in_reply_to"; } static constexpr reltypeid_t Annotation() { return "m.annotation"; } @@ -35,15 +38,15 @@ struct EventRelation static EventRelation replyTo(QString eventId) { - return EventRelation { Reply(), std::move(eventId) }; + return EventRelation(Reply(), std::move(eventId)); } static EventRelation annotate(QString eventId, QString key) { - return EventRelation { Annotation(), std::move(eventId), std::move(key) }; + return EventRelation(Annotation(), std::move(eventId), std::move(key)); } static EventRelation replace(QString eventId) { - return EventRelation { Replacement(), std::move(eventId) }; + return EventRelation(Replacement(), std::move(eventId)); } }; template <> @@ -70,8 +73,8 @@ public: return content(QStringLiteral("m.relates_to")); } -private: - EventRelation _relation; +//private: +// EventRelation _relation; }; REGISTER_EVENT_TYPE(ReactionEvent) -- cgit v1.2.3