diff --git a/Quotient/events/redactionevent.h b/Quotient/events/redactionevent.h index a2e0b73b5..2830dd947 100644 --- a/Quotient/events/redactionevent.h +++ b/Quotient/events/redactionevent.h @@ -12,10 +12,21 @@ class QUOTIENT_API RedactionEvent : public RoomEvent { using RoomEvent::RoomEvent; + [[deprecated("Use redactedEvents() instead")]] QString redactedEvent() const { return fullJson()["redacts"_ls].toString(); } + QStringList redactedEvents() const + { + const auto evtIdJson = contentJson()["redacts"_ls]; + if (evtIdJson.isArray()) + return fromJson(evtIdJson); // MSC2244: a list of ids + if (evtIdJson.isString()) + return { fromJson(evtIdJson) }; // MSC2174: id in content + return { fullJson()["redacts"_ls].toString() }; // legacy fallback + } + QUO_CONTENT_GETTER(QString, reason) }; } // namespace Quotient diff --git a/Quotient/room.cpp b/Quotient/room.cpp index fcc7485b4..dce5a5732 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -343,11 +343,10 @@ class Q_DECL_HIDDEN Room::Private { /*! Apply redaction to the timeline * - * 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 + * Tries to find events in the timeline and redact them. + * \return the list of event ids that were NOT found and redacted */ - bool processRedaction(const RedactionEvent& redaction); + QStringList processRedaction(const RedactionEvent& redaction); /*! Apply a new revision of the event to the timeline * @@ -2836,59 +2835,67 @@ RoomEventPtr makeRedacted(const RoomEvent& target, return loadEvent(originalJson); } -bool Room::Private::processRedaction(const RedactionEvent& redaction) +QStringList Room::Private::processRedaction(const RedactionEvent& redaction) { + QStringList unredactedIds; // Can't use findInTimeline because it returns a const iterator, and // we need to change the underlying TimelineItem. - const auto pIdx = eventsIndex.constFind(redaction.redactedEvent()); - if (pIdx == eventsIndex.cend()) - return false; - - Q_ASSERT(q->isValidIndex(*pIdx)); + const auto& eventIds = redaction.redactedEvents(); + for (const auto& evtId: eventIds) { + const auto pIdx = eventsIndex.constFind(evtId); + if (pIdx == eventsIndex.cend()) { + unredactedIds.push_back(evtId); + continue; + } - auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; - if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) { - qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event" - << ti->id() << "already done, skipping"; - return true; - } - if (ti->is()) - FileMetadataMap::remove(id, ti->id()); + Q_ASSERT(q->isValidIndex(*pIdx)); - // 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(makeRedacted(*ti, redaction)); - qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); - if (oldEvent->isStateEvent()) { - // Check whether the old event was a part of current state; if it was, - // update the current state to the redacted event object. - const auto currentStateEvt = - currentState.get(oldEvent->matrixType(), oldEvent->stateKey()); - Q_ASSERT(currentStateEvt); - if (currentStateEvt == oldEvent.get()) { - // Historical states can't be in currentState - Q_ASSERT(ti.index() >= 0); - qCDebug(STATE).nospace() - << "Redacting state " << oldEvent->matrixType() << "/" - << oldEvent->stateKey(); - // Retarget the current state to the newly made event. - if (q->processStateEvent(*ti)) - emit q->namesChanged(q); - updateDisplayname(); + auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; + if (ti->isRedacted() && ti->redactedBecause()->id() == redaction.id()) { + qCDebug(EVENTS) << "Redaction" << redaction.id() << "of event" + << ti->id() << "already done, skipping"; + continue; } - } - if (const auto* reaction = eventCast(oldEvent)) { - const auto& content = reaction->content().value; - const std::pair lookupKey { content.eventId, content.type }; - if (relations.contains(lookupKey)) { - relations[lookupKey].removeOne(reaction); - emit q->updatedEvent(content.eventId); + if (ti->is()) + FileMetadataMap::remove(id, ti->id()); + + + // 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(makeRedacted(*ti, redaction)); + qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" + << redaction.id(); + if (oldEvent->isStateEvent()) { + // Check whether the old event was a part of current state; if it was, + // update the current state to the redacted event object. + const auto currentStateEvt = + currentState.get(oldEvent->matrixType(), oldEvent->stateKey()); + Q_ASSERT(currentStateEvt); + if (currentStateEvt == oldEvent.get()) { + // Historical states can't be in currentState + Q_ASSERT(ti.index() >= 0); + qCDebug(STATE).nospace() + << "Redacting state " << oldEvent->matrixType() << "/" + << oldEvent->stateKey(); + // Retarget the current state to the newly made event. + if (q->processStateEvent(*ti)) + emit q->namesChanged(q); + updateDisplayname(); + } + } + if (const auto* reaction = eventCast(oldEvent)) { + const auto& content = reaction->content().value; + const std::pair lookupKey { content.eventId, content.type }; + if (relations.contains(lookupKey)) { + relations[lookupKey].removeOne(reaction); + emit q->updatedEvent(conten.eventId); + } } + q->onRedaction(*oldEvent, *ti); + emit q->replacedEvent(ti.event(), std::to_address(oldEvent)); + // By now, all references to oldEvent must have been updated to ti.event() } - q->onRedaction(*oldEvent, *ti); - emit q->replacedEvent(ti.event(), std::to_address(oldEvent)); - // By now, all references to oldEvent must have been updated to ti.event() - return true; + return unredactedIds; } /** Make a replaced event @@ -3011,19 +3018,22 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) 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; - if (auto targetIt = std::find_if(events.begin(), events.end(), - [id = r->redactedEvent()](const RoomEventPtr& ep) { - return ep->id() == id; - }); targetIt != events.end()) - *targetIt = makeRedacted(**targetIt, *r); - else - qCDebug(STATE) - << "Redaction" << r->id() << "ignored: target event" - << r->redactedEvent() << "is not found"; - // If the target event comes later, it comes already redacted. + // Try to find the targets in the timeline, then in the batch. + const auto unredactedIds = processRedaction(*r); + for (const auto& idToRedact: unredactedIds) { + if (auto targetIt = + std::find_if(events.begin(), it, + [&idToRedact](const RoomEventPtr& ep) { + return ep->id() == idToRedact; + }); + targetIt != it) + *targetIt = makeRedacted(**targetIt, *r); + else + qCDebug(EVENTS) + << "Target event" << idToRedact << "in redaction" + << r->id() << "is not found"; + // If the target event comes later, it comes already redacted. + } } if (auto* msg = eventCast(eptr); msg && !msg->replacedEvent().isEmpty()) {