aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/connection.cpp15
-rw-r--r--lib/connection.h7
-rw-r--r--lib/events/roomevent.cpp5
-rw-r--r--lib/events/roomevent.h1
-rw-r--r--lib/jobs/sendeventjob.cpp45
-rw-r--r--lib/jobs/sendeventjob.h57
-rw-r--r--lib/room.cpp209
-rw-r--r--lib/room.h23
-rw-r--r--lib/util.h18
9 files changed, 201 insertions, 179 deletions
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<SendEventJob>(room->id(), type, message);
-}
-
PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const
{
return callApi<PostReceiptJob>(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<SendMessageJob>(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<PostMessageJob>() or Room::postMessage() instead */
- virtual void postMessage(Room* room, const QString& type,
- const QString& message) const;
/** @deprecated Use callApi<PostReceiptJob>() 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 <kde@fxrh.de>
- *
- * 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 <kde@fxrh.de>
- *
- * 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 <typename EvT>
- 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..4b349f44 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<QString, TimelineItem::index_t> eventsIndex;
QString id;
QStringList aliases;
@@ -174,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
@@ -184,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;
@@ -199,9 +199,18 @@ class Room::Private
void markMessagesAsRead(rev_iter_t upToMarker);
+ QString sendEvent(RoomEventPtr&& event);
+
+ template <typename EventT, typename... ArgTs>
+ QString sendEvent(ArgTs&&... eventArgs)
+ {
+ return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...));
+ }
+
template <typename EvT>
auto requestSetState(const QString& stateKey, const EvT& event)
{
+ // TODO: Queue up state events sending (see #133).
return connection->callApi<SetRoomStateWithKeyJob>(
id, EvT::matrixTypeId(), stateKey, event.contentJson());
}
@@ -274,6 +283,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;
@@ -937,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
@@ -956,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<const RedactionEvent>(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<RedactionEvent>(*e))
+ continue;
+#endif
timeline.emplace_front(move(e), --index);
+ }
else
+ {
+ if (auto* redEvt = eventCast<const RedactionEvent>(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
@@ -1073,32 +1102,65 @@ void Room::updateData(SyncRoomData&& data)
}
}
-void Room::postMessage(const QString& type, const QString& plainText)
+QString Room::Private::sendEvent(RoomEventPtr&& event)
{
- postMessage(RoomMessageEvent { plainText, type });
+ 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());
+ });
+ return pEvent->transactionId();
}
-void Room::postMessage(const QString& plainText, MessageEventType type)
+QString Room::postMessage(const QString& type, const QString& plainText)
{
- postMessage(RoomMessageEvent { plainText, type });
+ return d->sendEvent<RoomMessageEvent>(plainText, type);
}
-void Room::postHtmlMessage(const QString& plainText, const QString& htmlText,
- MessageEventType type)
+QString Room::postMessage(const QString& plainText, MessageEventType type)
{
- postMessage(RoomMessageEvent { plainText, type,
- new EventContent::TextContent(htmlText, QStringLiteral("text/html")) });
+ return d->sendEvent<RoomMessageEvent>(plainText, type);
+}
+QString Room::postHtmlMessage(const QString& plainText, const QString& htmlText,
+ MessageEventType type)
+{
+ return d->sendEvent<RoomMessageEvent>(plainText, type,
+ new EventContent::TextContent(htmlText, QStringLiteral("text/html")));
}
-void Room::postMessage(const RoomMessageEvent& event)
+QString Room::postMessage(RoomEvent* event)
{
if (usesEncryption())
{
qCCritical(MAIN) << "Room" << displayName()
<< "enforces encryption; sending encrypted messages is not supported yet";
}
- connection()->callApi<SendEventJob>(id(), event);
+ return d->sendEvent(RoomEventPtr(event));
+}
+
+QString Room::postMessage(const QString& matrixType,
+ const QJsonObject& eventContent)
+{
+ return d->sendEvent(loadEvent<RoomEvent>(basicEventJson(matrixType, eventContent)));
}
void Room::setName(const QString& newName)
@@ -1116,6 +1178,26 @@ void Room::setTopic(const QString& newTopic)
d->requestSetState(RoomTopicEvent(newTopic));
}
+bool isEchoEvent(const RoomEventPtr& le, const RoomEventPtr& re)
+{
+ 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)
{
d->getPreviousContent(limit);
@@ -1387,39 +1469,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())
- emit q->aboutToAddNewMessages(normalEvents);
- const auto insertedSize = moveEventsToTimeline(normalEvents, Newer);
- const auto from = timeline.cend() - insertedSize;
- if (insertedSize > 0)
+ auto totalInserted = 0;
+ for (auto it = events.begin(); it != events.end();)
{
- qCDebug(MAIN)
- << "Room" << displayname << "received" << insertedSize
- << "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<RedactionEvent>(r));
+ auto nextPendingPair = findFirstOf(it, events.end(),
+ unsyncedEvents.begin(), unsyncedEvents.end(), isEchoEvent);
+ auto nextPending = nextPendingPair.first;
+
+ 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();
}
-#endif
- if (insertedSize > 0)
+
+ if (totalInserted > 0)
{
- emit q->addedMessages();
+ qCDebug(MAIN)
+ << "Room" << displayname << "received" << totalInserted
+ << "new events; the last event is now" << timeline.back();
+ 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
@@ -1437,7 +1529,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)
@@ -1445,18 +1537,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
@@ -1648,7 +1733,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..5cac615a 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
@@ -346,14 +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);
- void postMessage(const RoomMessageEvent& event);
+ 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 */
+ 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);
@@ -384,6 +389,12 @@ namespace QMatrixClient
void aboutToAddHistoricalMessages(RoomEventsRange events);
void aboutToAddNewMessages(RoomEventsRange events);
void addedMessages();
+ void pendingEventAboutToAdd();
+ void pendingEventAdded();
+ void pendingEventAboutToMerge(RoomEvent* serverEvent,
+ int pendingEventIndex);
+ void pendingEventMerged();
+ void pendingEventChanged(int pendingEventIndex);
/**
* @brief The room name, the canonical alias or other aliases changed
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<typename InputIt, typename ForwardIt, typename Pred>
+ inline std::pair<InputIt, ForwardIt> 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