From baee19241daffd50e0b32559cda64d5b6ede09a2 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 13 Jun 2017 17:30:13 +0900 Subject: Initial support for local echo The Room class has gained a new internal container, unsyncedEvents, storing locally-created Event objects that are about to be sent or are sent but not yet synced. These objects are supposed to be complete enough to be displayed by clients in a usual way; access to them is provided by Room::pendingEvents() accessor. A set of pendingEvent* signals has been added to notify clients about changes in this container (adding, removal, status update). Yet unsent events don't have Event::id() at all; sent but yet unsynced ones have Event::id() but have almost nothing else except the content for now (probably a sender and an (at least local) timestamp are worth adding). Also: SendEventJob is removed in favor of GTAD-generated SendMessageJob. --- lib/room.cpp | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 9 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index a8007f20..ca29eca5 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -27,13 +27,13 @@ #include "csapi/account-data.h" #include "csapi/message_pagination.h" #include "csapi/room_state.h" +#include "csapi/room_send.h" #include "events/simplestateevents.h" #include "events/roomavatarevent.h" #include "events/roommemberevent.h" #include "events/typingevent.h" #include "events/receiptevent.h" #include "events/redactionevent.h" -#include "jobs/sendeventjob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/downloadfilejob.h" #include "jobs/postreadmarkersjob.h" @@ -88,6 +88,7 @@ class Room::Private Connection* connection; Timeline timeline; + RoomEvents unsyncedEvents; QHash eventsIndex; QString id; QStringList aliases; @@ -199,9 +200,20 @@ class Room::Private void markMessagesAsRead(rev_iter_t upToMarker); + void sendEvent(RoomEventPtr&& event); + + template + void sendEvent(ArgTs&&... eventArgs) + { + sendEvent(makeEvent(std::forward(eventArgs)...)); + } + + void deleteLocalEcho(const RoomEventPtr& remoteEcho); + template auto requestSetState(const QString& stateKey, const EvT& event) { + // TODO: Queue up state events sending (see #133). return connection->callApi( id, EvT::matrixTypeId(), stateKey, event.contentJson()); } @@ -274,6 +286,11 @@ const Room::Timeline& Room::messageEvents() const return d->timeline; } +const RoomEvents& Room::pendingEvents() const +{ + return d->unsyncedEvents; +} + QString Room::name() const { return d->name; @@ -1073,32 +1090,64 @@ void Room::updateData(SyncRoomData&& data) } } +void Room::Private::sendEvent(RoomEventPtr&& event) +{ + auto* pEvent = rawPtr(event); + emit q->pendingEventAboutToAdd(); + unsyncedEvents.emplace_back(move(event)); + emit q->pendingEventAdded(); + + if (pEvent->transactionId().isEmpty()) + pEvent->setTransactionId(connection->generateTxnId()); + // TODO: Enqueue the job rather than immediately trigger it + auto call = connection->sendMessage(id, *pEvent); + Room::connect(call, &BaseJob::success, q, [this,call,pEvent] + { + const auto comparator = + [pEvent] (const auto& eptr) { return rawPtr(eptr) == pEvent; }; + + // Find an event by the pointer saved in the lambda + auto it = std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), + comparator); + if (it == unsyncedEvents.end()) + return; // The event is already synced, nothing to do + + pEvent->addId(call->eventId()); + emit q->pendingEventChanged(it - unsyncedEvents.begin()); + }); +} + void Room::postMessage(const QString& type, const QString& plainText) { - postMessage(RoomMessageEvent { plainText, type }); + d->sendEvent(plainText, type); } void Room::postMessage(const QString& plainText, MessageEventType type) { - postMessage(RoomMessageEvent { plainText, type }); + d->sendEvent(plainText, type); } void Room::postHtmlMessage(const QString& plainText, const QString& htmlText, MessageEventType type) { - postMessage(RoomMessageEvent { plainText, type, - new EventContent::TextContent(htmlText, QStringLiteral("text/html")) }); - + d->sendEvent(plainText, type, + new EventContent::TextContent(htmlText, QStringLiteral("text/html"))); } -void Room::postMessage(const RoomMessageEvent& event) +void Room::postMessage(RoomEvent* event) { if (usesEncryption()) { qCCritical(MAIN) << "Room" << displayName() << "enforces encryption; sending encrypted messages is not supported yet"; } - connection()->callApi(id(), event); + d->sendEvent(RoomEventPtr(event)); +} + +void Room::postMessage(const QString& matrixType, + const QJsonObject& eventContent) +{ + d->sendEvent(loadEvent(basicEventJson(matrixType, eventContent))); } void Room::setName(const QString& newName) @@ -1116,6 +1165,41 @@ void Room::setTopic(const QString& newTopic) d->requestSetState(RoomTopicEvent(newTopic)); } +void Room::Private::deleteLocalEcho(const RoomEventPtr& remoteEcho) +{ + if (remoteEcho->senderId() == connection->userId()) + { + auto localEchoIt = + std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), + [&remoteEcho] (const RoomEventPtr& le) + { + if (le->type() != remoteEcho->type()) + return false; + + if (!le->id().isEmpty()) + return le->id() == remoteEcho->id(); + if (!le->transactionId().isEmpty()) + return le->transactionId() == + remoteEcho->transactionId(); + + // This one is not reliable (there can be two unsynced + // events with the same type, sender and state key) but + // it's the best we have for state events. + if (le->isStateEvent()) + return le->stateKey() == remoteEcho->stateKey(); + + // Empty id and no state key, hmm... (shrug) + return le->contentJson() == remoteEcho->contentJson(); + }); + if (localEchoIt != unsyncedEvents.end()) + { + emit q->pendingEventAboutToRemove(localEchoIt - unsyncedEvents.begin()); + unsyncedEvents.erase(localEchoIt); + emit q->pendingEventRemoved(); + } + } +} + void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); @@ -1399,7 +1483,12 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) #endif if (!normalEvents.empty()) + { + for (const auto& e: normalEvents) + deleteLocalEcho(e); + emit q->aboutToAddNewMessages(normalEvents); + } const auto insertedSize = moveEventsToTimeline(normalEvents, Newer); const auto from = timeline.cend() - insertedSize; if (insertedSize > 0) @@ -1648,7 +1737,7 @@ void Room::processAccountDataEvent(EventPtr&& event) // efficient; maaybe do it another day if (!currentData || currentData->contentJson() != event->contentJson()) { - currentData = std::move(event); + currentData = move(event); qCDebug(MAIN) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); -- cgit v1.2.3 From 6559a63cdd245279d0adb02994ffc043a794733f Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 27 Jul 2018 12:15:28 +0900 Subject: Provide a way to match the echo against the synced event This needed to split the container with the freshly arrived events into parts that don't have local echo and echo'ed events, and add them to the timeline emitting two different pairs of signals. Instead of being removed, pending events are now merged (effectively they are removed from unsyncedEvents container anyway but models can represent this as an echo event being "transformed" into a full-fledged one on a timeline). --- lib/room.cpp | 167 +++++++++++++++++++++++++++++------------------------------ lib/room.h | 5 +- 2 files changed, 84 insertions(+), 88 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index ca29eca5..dc60445d 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -175,8 +175,7 @@ class Room::Private void addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); - /** - * @brief Move events into the timeline + /** Move events into the timeline * * Insert events into the timeline, either new or historical. * Pointers in the original container become empty, the ownership @@ -185,11 +184,11 @@ class Room::Private * @param placement - position and direction of insertion: Older for * historical messages, Newer for new ones */ - Timeline::size_type moveEventsToTimeline(RoomEventsRange events, - EventsPlacement placement); + Timeline::difference_type moveEventsToTimeline(RoomEventsRange events, + EventsPlacement placement); /** - * Removes events from the passed container that are already in the timeline + * Remove events from the passed container that are already in the timeline */ void dropDuplicateEvents(RoomEvents& events) const; @@ -208,8 +207,6 @@ class Room::Private sendEvent(makeEvent(std::forward(eventArgs)...)); } - void deleteLocalEcho(const RoomEventPtr& remoteEcho); - template auto requestSetState(const QString& stateKey, const EvT& event) { @@ -954,7 +951,7 @@ inline auto makeErrorStr(const Event& e, QByteArray msg) return msg.append("; event dump follows:\n").append(e.originalJson()); } -Room::Timeline::size_type Room::Private::moveEventsToTimeline( +Room::Timeline::difference_type Room::Private::moveEventsToTimeline( RoomEventsRange events, EventsPlacement placement) { // Historical messages arrive in newest-to-oldest order, so the process for @@ -973,18 +970,33 @@ Room::Timeline::size_type Room::Private::moveEventsToTimeline( Q_ASSERT_X(!eventsIndex.contains(eId), __FUNCTION__, makeErrorStr(*e, "Event is already in the timeline; " "incoming events were not properly deduplicated")); - if (auto* redEvt = eventCast(e)) - processRedaction(redEvt); if (placement == Older) + { + // No need to process redaction events here: historical redacted + // events already come redacted. +#ifndef KEEP_REDACTIONS_IN_TIMELINE + if (is(*e)) + continue; +#endif timeline.emplace_front(move(e), --index); + } else + { + if (auto* redEvt = eventCast(e)) + { + processRedaction(redEvt); +#ifndef KEEP_REDACTIONS_IN_TIMELINE + continue; +#endif + } timeline.emplace_back(move(e), ++index); + } eventsIndex.insert(eId, index); Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId); } - // Pointers in "events" are empty now, but events.size() didn't change - Q_ASSERT(int(events.size()) == (index - baseIndex) * int(placement)); - return events.size(); + const auto insertedSize = (index - baseIndex) * int(placement); + Q_ASSERT(insertedSize >= 0); + return insertedSize; } QString Room::roomMembername(const User* u) const @@ -1165,39 +1177,24 @@ void Room::setTopic(const QString& newTopic) d->requestSetState(RoomTopicEvent(newTopic)); } -void Room::Private::deleteLocalEcho(const RoomEventPtr& remoteEcho) +bool isEchoEvent(const RoomEventPtr& le, const RoomEventPtr& re) { - if (remoteEcho->senderId() == connection->userId()) - { - auto localEchoIt = - std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), - [&remoteEcho] (const RoomEventPtr& le) - { - if (le->type() != remoteEcho->type()) - return false; - - if (!le->id().isEmpty()) - return le->id() == remoteEcho->id(); - if (!le->transactionId().isEmpty()) - return le->transactionId() == - remoteEcho->transactionId(); - - // This one is not reliable (there can be two unsynced - // events with the same type, sender and state key) but - // it's the best we have for state events. - if (le->isStateEvent()) - return le->stateKey() == remoteEcho->stateKey(); - - // Empty id and no state key, hmm... (shrug) - return le->contentJson() == remoteEcho->contentJson(); - }); - if (localEchoIt != unsyncedEvents.end()) - { - emit q->pendingEventAboutToRemove(localEchoIt - unsyncedEvents.begin()); - unsyncedEvents.erase(localEchoIt); - emit q->pendingEventRemoved(); - } - } + if (le->type() != re->type()) + return false; + + if (!le->id().isEmpty()) + return le->id() == re->id(); + if (!le->transactionId().isEmpty()) + return le->transactionId() == re->transactionId(); + + // This one is not reliable (there can be two unsynced + // events with the same type, sender and state key) but + // it's the best we have for state events. + if (le->isStateEvent()) + return le->stateKey() == re->stateKey(); + + // Empty id and no state key, hmm... (shrug) + return le->contentJson() == re->contentJson(); } void Room::getPreviousContent(int limit) @@ -1471,44 +1468,49 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) auto timelineSize = timeline.size(); dropDuplicateEvents(events); -#ifndef KEEP_REDACTIONS_IN_TIMELINE - // We want to process redactions in the order of arrival (covering the - // case of one redaction superseding another one), hence stable partition. - const auto normalsBegin = - stable_partition(events.begin(), events.end(), isRedaction); - RoomEventsRange redactions { events.begin(), normalsBegin }, - normalEvents { normalsBegin, events.end() }; -#else - RoomEventsRange normalEvents { events }; -#endif + if (events.empty()) + return; - if (!normalEvents.empty()) + auto totalInserted = 0; + for (auto it = events.begin(); it != events.end();) { - for (const auto& e: normalEvents) - deleteLocalEcho(e); + auto nextPendingPair = findFirstOf(it, events.end(), + unsyncedEvents.begin(), unsyncedEvents.end(), isEchoEvent); + auto nextPending = nextPendingPair.first; - emit q->aboutToAddNewMessages(normalEvents); + if (it != nextPending) + { + RoomEventsRange eventsSpan { it, nextPending }; + emit q->aboutToAddNewMessages(eventsSpan); + if (auto insertedSize = moveEventsToTimeline(eventsSpan, Newer)) + { + totalInserted += insertedSize; + q->onAddNewTimelineEvents(timeline.cend() - insertedSize); + } + emit q->addedMessages(); + } + if (nextPending == events.end()) + break; + + it = nextPending + 1; + emit q->pendingEventAboutToMerge(nextPending->get(), + nextPendingPair.second - unsyncedEvents.begin()); + unsyncedEvents.erase(nextPendingPair.second); + if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer)) + { + totalInserted += insertedSize; + q->onAddNewTimelineEvents(timeline.cend() - insertedSize); + } + emit q->pendingEventMerged(); } - const auto insertedSize = moveEventsToTimeline(normalEvents, Newer); - const auto from = timeline.cend() - insertedSize; - if (insertedSize > 0) + + if (totalInserted > 0) { qCDebug(MAIN) - << "Room" << displayname << "received" << insertedSize + << "Room" << displayname << "received" << totalInserted << "new events; the last event is now" << timeline.back(); - q->onAddNewTimelineEvents(from); - } -#ifndef KEEP_REDACTIONS_IN_TIMELINE - for (const auto& r: redactions) - { - Q_ASSERT(isRedaction(r)); - processRedaction(eventCast(r)); - } -#endif - if (insertedSize > 0) - { - emit q->addedMessages(); + const auto from = timeline.cend() - totalInserted; // The first event in the just-added batch (referred to by `from`) // defines whose read marker can possibly be promoted any further over // the same author's events newly arrived. Others will need explicit @@ -1526,7 +1528,7 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); } - Q_ASSERT(timeline.size() == timelineSize + insertedSize); + Q_ASSERT(timeline.size() == timelineSize + totalInserted); } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) @@ -1534,18 +1536,11 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) const auto timelineSize = timeline.size(); dropDuplicateEvents(events); -#ifndef KEEP_REDACTIONS_IN_TIMELINE - const auto redactionsBegin = - remove_if(events.begin(), events.end(), isRedaction); - RoomEventsRange normalEvents { events.begin(), redactionsBegin }; -#else - RoomEventsRange normalEvents { events }; -#endif - if (normalEvents.empty()) + if (events.empty()) return; - emit q->aboutToAddHistoricalMessages(normalEvents); - const auto insertedSize = moveEventsToTimeline(normalEvents, Older); + emit q->aboutToAddHistoricalMessages(events); + const auto insertedSize = moveEventsToTimeline(events, Older); const auto from = timeline.crend() - insertedSize; qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize diff --git a/lib/room.h b/lib/room.h index 01a7389c..d72f7c7e 100644 --- a/lib/room.h +++ b/lib/room.h @@ -390,8 +390,9 @@ namespace QMatrixClient void addedMessages(); void pendingEventAboutToAdd(); void pendingEventAdded(); - void pendingEventAboutToRemove(int pendingEventIndex); - void pendingEventRemoved(); + void pendingEventAboutToMerge(RoomEvent* serverEvent, + int pendingEventIndex); + void pendingEventMerged(); void pendingEventChanged(int pendingEventIndex); /** -- cgit v1.2.3 From f1e57caac169e53194111d05b16153d13a400bfb Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 27 Jul 2018 15:15:47 +0900 Subject: Make Room::postMessage and Room::postHtmlMessage return transactionId This is at least some (actually, not even that bad) identification of a message. Ideally it would probably be to return some handler that would allow to track the end-to-end status of the event - from getting sent to landing in the timeline. Right now the experience is crippled - transactionId always exists but only works for message events and Room has no way to give the event status by transactionId. pendingEvent* signals are somewhat helping, though. --- lib/room.cpp | 29 +++++++++++++++-------------- lib/room.h | 17 +++++++++-------- 2 files changed, 24 insertions(+), 22 deletions(-) (limited to 'lib/room.cpp') diff --git a/lib/room.cpp b/lib/room.cpp index dc60445d..4b349f44 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -199,12 +199,12 @@ class Room::Private void markMessagesAsRead(rev_iter_t upToMarker); - void sendEvent(RoomEventPtr&& event); + QString sendEvent(RoomEventPtr&& event); template - void sendEvent(ArgTs&&... eventArgs) + QString sendEvent(ArgTs&&... eventArgs) { - sendEvent(makeEvent(std::forward(eventArgs)...)); + return sendEvent(makeEvent(std::forward(eventArgs)...)); } template @@ -1102,7 +1102,7 @@ void Room::updateData(SyncRoomData&& data) } } -void Room::Private::sendEvent(RoomEventPtr&& event) +QString Room::Private::sendEvent(RoomEventPtr&& event) { auto* pEvent = rawPtr(event); emit q->pendingEventAboutToAdd(); @@ -1127,39 +1127,40 @@ void Room::Private::sendEvent(RoomEventPtr&& event) pEvent->addId(call->eventId()); emit q->pendingEventChanged(it - unsyncedEvents.begin()); }); + return pEvent->transactionId(); } -void Room::postMessage(const QString& type, const QString& plainText) +QString Room::postMessage(const QString& type, const QString& plainText) { - d->sendEvent(plainText, type); + return d->sendEvent(plainText, type); } -void Room::postMessage(const QString& plainText, MessageEventType type) +QString Room::postMessage(const QString& plainText, MessageEventType type) { - d->sendEvent(plainText, type); + return d->sendEvent(plainText, type); } -void Room::postHtmlMessage(const QString& plainText, const QString& htmlText, +QString Room::postHtmlMessage(const QString& plainText, const QString& htmlText, MessageEventType type) { - d->sendEvent(plainText, type, + return d->sendEvent(plainText, type, new EventContent::TextContent(htmlText, QStringLiteral("text/html"))); } -void Room::postMessage(RoomEvent* event) +QString Room::postMessage(RoomEvent* event) { if (usesEncryption()) { qCCritical(MAIN) << "Room" << displayName() << "enforces encryption; sending encrypted messages is not supported yet"; } - d->sendEvent(RoomEventPtr(event)); + return d->sendEvent(RoomEventPtr(event)); } -void Room::postMessage(const QString& matrixType, +QString Room::postMessage(const QString& matrixType, const QJsonObject& eventContent) { - d->sendEvent(loadEvent(basicEventJson(matrixType, eventContent))); + return d->sendEvent(loadEvent(basicEventJson(matrixType, eventContent))); } void Room::setName(const QString& newName) diff --git a/lib/room.h b/lib/room.h index d72f7c7e..5cac615a 100644 --- a/lib/room.h +++ b/lib/room.h @@ -347,17 +347,18 @@ namespace QMatrixClient void setJoinState( JoinState state ); public slots: - void postMessage(const QString& plainText, - MessageEventType type = MessageEventType::Text); - void postHtmlMessage(const QString& plainText, const QString& htmlText, - MessageEventType type = MessageEventType::Text); + QString postMessage(const QString& plainText, + MessageEventType type = MessageEventType::Text); + QString postHtmlMessage( + const QString& plainText, const QString& htmlText, + MessageEventType type = MessageEventType::Text); /** Post a pre-created room message event; takes ownership of the event */ - void postMessage(RoomEvent* event); - void postMessage(const QString& matrixType, - const QJsonObject& eventContent); + QString postMessage(RoomEvent* event); + QString postMessage(const QString& matrixType, + const QJsonObject& eventContent); /** @deprecated If you have a custom event type, construct the event * and pass it as a whole to postMessage() */ - void postMessage(const QString& type, const QString& plainText); + QString postMessage(const QString& type, const QString& plainText); void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); void setTopic(const QString& newTopic); -- cgit v1.2.3