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. --- CMakeLists.txt | 1 - examples/qmc-example.cpp | 8 ++-- lib/connection.cpp | 15 ++++--- lib/connection.h | 7 +-- lib/events/roomevent.cpp | 5 +++ lib/events/roomevent.h | 1 + lib/jobs/sendeventjob.cpp | 45 ------------------- lib/jobs/sendeventjob.h | 57 ------------------------ lib/room.cpp | 107 ++++++++++++++++++++++++++++++++++++++++++---- lib/room.h | 11 ++++- libqmatrixclient.pri | 2 - 11 files changed, 131 insertions(+), 128 deletions(-) delete mode 100644 lib/jobs/sendeventjob.cpp delete mode 100644 lib/jobs/sendeventjob.h diff --git a/CMakeLists.txt b/CMakeLists.txt index a377ec4d..7be054e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,6 @@ set(libqmatrixclient_SRCS lib/events/directchatevent.cpp lib/jobs/requestdata.cpp lib/jobs/basejob.cpp - lib/jobs/sendeventjob.cpp lib/jobs/syncjob.cpp lib/jobs/mediathumbnailjob.cpp lib/jobs/downloadfilejob.cpp diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index 4de42623..d24b6ed0 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -2,7 +2,7 @@ #include "connection.h" #include "room.h" #include "user.h" -#include "jobs/sendeventjob.h" +#include "csapi/room_send.h" #include "csapi/joining.h" #include "csapi/leaving.h" @@ -87,8 +87,8 @@ void QMCTest::setup(const QString& testRoomName) c->sync(10000); else if (targetRoom) { - auto j = c->callApi(targetRoom->id(), - RoomMessageEvent(origin % ": All tests finished")); + auto j = c->sendMessage(targetRoom->id(), + RoomMessageEvent(origin % ": All tests finished")); connect(j, &BaseJob::finished, this, &QMCTest::leave); } else @@ -176,7 +176,7 @@ void QMCTest::sendAndRedact() { running.push_back("Redaction"); cout << "Sending a message to redact" << endl; - auto* job = targetRoom->connection()->callApi(targetRoom->id(), + auto* job = targetRoom->connection()->sendMessage(targetRoom->id(), RoomMessageEvent(origin % ": Message to redact")); connect(job, &BaseJob::success, targetRoom, [job,this] { cout << "Message to redact has been succesfully sent, redacting" << endl; diff --git a/lib/connection.cpp b/lib/connection.cpp index 8007cea1..d3a53cf4 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -30,7 +30,7 @@ #include "csapi/account-data.h" #include "csapi/joining.h" #include "csapi/to_device.h" -#include "jobs/sendeventjob.h" +#include "csapi/room_send.h" #include "jobs/syncjob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/downloadfilejob.h" @@ -405,11 +405,6 @@ void Connection::stopSync() } } -void Connection::postMessage(Room* room, const QString& type, const QString& message) const -{ - callApi(room->id(), type, message); -} - PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const { return callApi(room->id(), "m.read", event->id()); @@ -645,6 +640,14 @@ SendToDeviceJob* Connection::sendToDevices(const QString& eventType, eventType, generateTxnId(), json); } +SendMessageJob* Connection::sendMessage(const QString& roomId, const RoomEvent& event) const +{ + const auto txnId = event.transactionId().isEmpty() + ? generateTxnId() : event.transactionId(); + return callApi(roomId, event.matrixType(), + txnId, event.contentJson()); +} + QUrl Connection::homeserver() const { return d->data->baseUrl(); diff --git a/lib/connection.h b/lib/connection.h index 7adab883..48ca2232 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -47,6 +47,7 @@ namespace QMatrixClient class GetContentJob; class DownloadFileJob; class SendToDeviceJob; + class SendMessageJob; /** Create a single-shot connection that triggers on the signal and * then self-disconnects @@ -421,11 +422,11 @@ namespace QMatrixClient SendToDeviceJob* sendToDevices(const QString& eventType, const UsersToDevicesToEvents& eventsMap) const; + SendMessageJob* sendMessage(const QString& roomId, + const RoomEvent& event) const; + // Old API that will be abolished any time soon. DO NOT USE. - /** @deprecated Use callApi() or Room::postMessage() instead */ - virtual void postMessage(Room* room, const QString& type, - const QString& message) const; /** @deprecated Use callApi() or Room::postReceipt() instead */ virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event) const; diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 3d09af8a..75850772 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -75,6 +75,11 @@ QString RoomEvent::redactionReason() const return isRedacted() ? _redactedBecause->reason() : QString{}; } +QString RoomEvent::stateKey() const +{ + return fullJson()["state_key"_ls].toString(); +} + void RoomEvent::addId(const QString& newId) { Q_ASSERT(id().isEmpty()); Q_ASSERT(!newId.isEmpty()); diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index d2bc6edc..fcbb33e5 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -57,6 +57,7 @@ namespace QMatrixClient { } QString redactionReason() const; const QString& transactionId() const { return _txnId; } + QString stateKey() const; /** * Sets the transaction id for locally created events. This should be diff --git a/lib/jobs/sendeventjob.cpp b/lib/jobs/sendeventjob.cpp deleted file mode 100644 index e5852c65..00000000 --- a/lib/jobs/sendeventjob.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 "sendeventjob.h" - -#include "events/roommessageevent.h" - -using namespace QMatrixClient; - -SendEventJob::SendEventJob(const QString& roomId, const QString& type, - const QString& plainText) - : SendEventJob(roomId, RoomMessageEvent(plainText, type)) -{ } - -void SendEventJob::beforeStart(const ConnectionData* connData) -{ - BaseJob::beforeStart(connData); - setApiEndpoint(apiEndpoint() + connData->generateTxnId()); -} - -BaseJob::Status SendEventJob::parseJson(const QJsonDocument& data) -{ - _eventId = data.object().value("event_id"_ls).toString(); - if (!_eventId.isEmpty()) - return Success; - - qCDebug(JOBS) << data; - return { UserDefinedError, "No event_id in the JSON response" }; -} - diff --git a/lib/jobs/sendeventjob.h b/lib/jobs/sendeventjob.h deleted file mode 100644 index af81ae26..00000000 --- a/lib/jobs/sendeventjob.h +++ /dev/null @@ -1,57 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach - * - * 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 "basejob.h" - -#include "connectiondata.h" - -namespace QMatrixClient -{ - class SendEventJob: public BaseJob - { - public: - /** Constructs a job that sends an arbitrary room event */ - template - SendEventJob(const QString& roomId, const EvT& event) - : BaseJob(HttpVerb::Put, QStringLiteral("SendEventJob"), - QStringLiteral("_matrix/client/r0/rooms/%1/send/%2/") - .arg(roomId, EvT::matrixTypeId()), // See also beforeStart() - Query(), - Data(event.contentJson())) - { } - - /** - * Constructs a plain text message job (for compatibility with - * the old PostMessageJob API). - */ - SendEventJob(const QString& roomId, const QString& type, - const QString& plainText); - - QString eventId() const { return _eventId; } - - protected: - Status parseJson(const QJsonDocument& data) override; - - private: - QString _eventId; - - void beforeStart(const ConnectionData* connData) override; - }; -} // namespace QMatrixClient 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()); diff --git a/lib/room.h b/lib/room.h index e7f260dd..01a7389c 100644 --- a/lib/room.h +++ b/lib/room.h @@ -216,6 +216,7 @@ namespace QMatrixClient Q_INVOKABLE QString roomMembername(const QString& userId) const; const Timeline& messageEvents() const; + const RoomEvents& pendingEvents() const; /** * A convenience method returning the read marker to the before-oldest * message @@ -350,7 +351,10 @@ namespace QMatrixClient MessageEventType type = MessageEventType::Text); void postHtmlMessage(const QString& plainText, const QString& htmlText, MessageEventType type = MessageEventType::Text); - void postMessage(const RoomMessageEvent& event); + /** Post a pre-created room message event; takes ownership of the event */ + void postMessage(RoomEvent* event); + void 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); @@ -384,6 +388,11 @@ namespace QMatrixClient void aboutToAddHistoricalMessages(RoomEventsRange events); void aboutToAddNewMessages(RoomEventsRange events); void addedMessages(); + void pendingEventAboutToAdd(); + void pendingEventAdded(); + void pendingEventAboutToRemove(int pendingEventIndex); + void pendingEventRemoved(); + void pendingEventChanged(int pendingEventIndex); /** * @brief The room name, the canonical alias or other aliases changed diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri index d3858de2..2f6a701a 100644 --- a/libqmatrixclient.pri +++ b/libqmatrixclient.pri @@ -33,7 +33,6 @@ HEADERS += \ $$SRCPATH/events/eventloader.h \ $$SRCPATH/jobs/requestdata.h \ $$SRCPATH/jobs/basejob.h \ - $$SRCPATH/jobs/sendeventjob.h \ $$SRCPATH/jobs/syncjob.h \ $$SRCPATH/jobs/mediathumbnailjob.h \ $$SRCPATH/jobs/downloadfilejob.h \ @@ -65,7 +64,6 @@ SOURCES += \ $$SRCPATH/events/directchatevent.cpp \ $$SRCPATH/jobs/requestdata.cpp \ $$SRCPATH/jobs/basejob.cpp \ - $$SRCPATH/jobs/sendeventjob.cpp \ $$SRCPATH/jobs/syncjob.cpp \ $$SRCPATH/jobs/mediathumbnailjob.cpp \ $$SRCPATH/jobs/downloadfilejob.cpp \ -- cgit v1.2.3 From a2ebdd4baa81d21a570792e6895ed8384e43c9c4 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 27 Jul 2018 12:10:14 +0900 Subject: util.h: findFirstOf A spin on the standard algorithm. --- lib/util.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/util.h b/lib/util.h index c491ff89..7769abce 100644 --- a/lib/util.h +++ b/lib/util.h @@ -191,6 +191,24 @@ namespace QMatrixClient iterator to; }; + /** A replica of std::find_first_of that returns a pair of iterators + * + * Convenient for cases when you need to know which particular "first of" + * [sFirst, sLast) has been found in [first, last). + */ + template + inline std::pair findFirstOf( + InputIt first, InputIt last, ForwardIt sFirst, ForwardIt sLast, + Pred pred) + { + for (; first != last; ++first) + for (auto it = sFirst; it != sLast; ++it) + if (pred(*first, *it)) + return std::make_pair(first, it); + + return std::make_pair(last, sLast); + } + /** A guard pointer that disconnects an interested object upon destruction * It's almost QPointer<> except that you have to initialise it with one * more additional parameter - a pointer to a QObject that will be -- 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(-) 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(-) 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 From c7e4d01c479452aad4616ee2d5a285f4fe0565aa Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 27 Jul 2018 14:31:28 +0900 Subject: Update tests (WIP) --- examples/qmc-example.cpp | 54 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp index d24b6ed0..8670b263 100644 --- a/examples/qmc-example.cpp +++ b/examples/qmc-example.cpp @@ -26,9 +26,10 @@ class QMCTest : public QObject void setup(const QString& testRoomName); void onNewRoom(Room* r); void startTests(); + void sendMessage(); void addAndRemoveTag(); void sendAndRedact(); - void checkRedactionOutcome(QString evtIdToRedact, + void checkRedactionOutcome(const QString& evtIdToRedact, RoomEventsRange events); void markDirectChat(); void checkDirectChatOutcome( @@ -87,9 +88,8 @@ void QMCTest::setup(const QString& testRoomName) c->sync(10000); else if (targetRoom) { - auto j = c->sendMessage(targetRoom->id(), - RoomMessageEvent(origin % ": All tests finished")); - connect(j, &BaseJob::finished, this, &QMCTest::leave); + targetRoom->postMessage(origin % ": All tests finished"); + connect(targetRoom, &Room::pendingEventMerged, this, &QMCTest::leave); } else finalize(); @@ -142,11 +142,32 @@ void QMCTest::onNewRoom(Room* r) void QMCTest::startTests() { cout << "Starting tests" << endl; + sendMessage(); addAndRemoveTag(); sendAndRedact(); markDirectChat(); } +void QMCTest::sendMessage() +{ + running.push_back("Message sending"); + cout << "Sending a message" << endl; + auto txnId = targetRoom->postMessage("Hello, " % origin % " is here"); + auto& pending = targetRoom->pendingEvents(); + if (pending.empty()) + { + QMC_CHECK("Message sending", false); + return; + } + auto it = std::find_if(pending.begin(), pending.end(), + [&txnId] (const RoomEventPtr& e) { + return e->transactionId() == txnId; + }); + QMC_CHECK("Message sending", it != pending.end()); + // TODO: Wait when it actually gets sent; check that it obtained an id + // Independently, check when it shows up in the timeline. +} + void QMCTest::addAndRemoveTag() { running.push_back("Tagging test"); @@ -176,19 +197,22 @@ void QMCTest::sendAndRedact() { running.push_back("Redaction"); cout << "Sending a message to redact" << endl; - auto* job = targetRoom->connection()->sendMessage(targetRoom->id(), - RoomMessageEvent(origin % ": Message to redact")); - connect(job, &BaseJob::success, targetRoom, [job,this] { - cout << "Message to redact has been succesfully sent, redacting" << endl; - targetRoom->redactEvent(job->eventId(), origin); - // Make sure to save the event id because the job is about to end. - connect(targetRoom, &Room::aboutToAddNewMessages, this, - std::bind(&QMCTest::checkRedactionOutcome, - this, job->eventId(), _1)); - }); + if (auto* job = targetRoom->connection()->sendMessage(targetRoom->id(), + RoomMessageEvent(origin % ": message to redact"))) + { + connect(job, &BaseJob::success, targetRoom, [job,this] { + cout << "Redacting the message" << endl; + targetRoom->redactEvent(job->eventId(), origin); + // Make sure to save the event id because the job is about to end. + connect(targetRoom, &Room::aboutToAddNewMessages, this, + std::bind(&QMCTest::checkRedactionOutcome, + this, job->eventId(), _1)); + }); + } else + QMC_CHECK("Redaction", false); } -void QMCTest::checkRedactionOutcome(QString evtIdToRedact, +void QMCTest::checkRedactionOutcome(const QString& evtIdToRedact, RoomEventsRange events) { static bool checkSucceeded = false; -- cgit v1.2.3