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/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 ++++- 8 files changed, 127 insertions(+), 121 deletions(-) delete mode 100644 lib/jobs/sendeventjob.cpp delete mode 100644 lib/jobs/sendeventjob.h (limited to 'lib') 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 -- cgit v1.2.3