aboutsummaryrefslogtreecommitdiff
path: root/lib/room.cpp
diff options
context:
space:
mode:
authorHubert Chathi <uhoreg@debian.org>2019-06-25 16:33:24 -0400
committerHubert Chathi <uhoreg@debian.org>2019-06-25 16:33:24 -0400
commit72d5660efd0755bb53a8699cd39865155400d288 (patch)
treeed7e7537e6a3eb7e8b92226c4015f9bfc8e11c5a /lib/room.cpp
parent52407a933bfe1fcc5f3aa1dccaa0b9a8279aa634 (diff)
parent681203f951d13e9e8eaf772435cac28c6d74cd42 (diff)
downloadlibquotient-72d5660efd0755bb53a8699cd39865155400d288.tar.gz
libquotient-72d5660efd0755bb53a8699cd39865155400d288.zip
Merge branch 'upstream' (v0.5.2)
Diffstat (limited to 'lib/room.cpp')
-rw-r--r--lib/room.cpp1133
1 files changed, 803 insertions, 330 deletions
diff --git a/lib/room.cpp b/lib/room.cpp
index ea771f17..9e7ff8d2 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -25,11 +25,14 @@
#include "csapi/receipts.h"
#include "csapi/redaction.h"
#include "csapi/account-data.h"
-#include "csapi/message_pagination.h"
#include "csapi/room_state.h"
#include "csapi/room_send.h"
+#include "csapi/rooms.h"
#include "csapi/tags.h"
+#include "csapi/room_upgrades.h"
#include "events/simplestateevents.h"
+#include "events/roomcreateevent.h"
+#include "events/roomtombstoneevent.h"
#include "events/roomavatarevent.h"
#include "events/roommemberevent.h"
#include "events/typingevent.h"
@@ -46,13 +49,15 @@
#include "connection.h"
#include "user.h"
#include "converters.h"
+#include "syncdata.h"
#include <QtCore/QHash>
#include <QtCore/QStringBuilder> // for efficient string concats (operator%)
-#include <QtCore/QElapsedTimer>
#include <QtCore/QPointer>
#include <QtCore/QDir>
#include <QtCore/QTemporaryFile>
+#include <QtCore/QRegularExpression>
+#include <QtCore/QMimeDatabase>
#include <array>
#include <functional>
@@ -67,12 +72,6 @@ using std::llround;
enum EventsPlacement : int { Older = -1, Newer = 1 };
-// A workaround for MSVC 2015 that fails with "error C2440: 'return':
-// cannot convert from 'initializer list' to 'QMatrixClient::FileTransferInfo'"
-#if (defined(_MSC_VER) && _MSC_VER < 1910) || (defined(__GNUC__) && __GNUC__ <= 4)
-# define WORKAROUND_EXTENDED_INITIALIZER_LIST
-#endif
-
class Room::Private
{
public:
@@ -86,29 +85,27 @@ class Room::Private
Room* q;
- // This updates the room displayname field (which is the way a room
- // should be shown in the room list) It should be called whenever the
- // list of members or the room name (m.room.name) or canonical alias change.
- void updateDisplayname();
-
Connection* connection;
+ QString id;
+ JoinState joinState;
+ RoomSummary summary = { none, 0, none };
+ /// The state of the room at timeline position before-0
+ /// \sa timelineBase
+ std::unordered_map<StateEventKey, StateEventPtr> baseState;
+ /// The state of the room at timeline position after-maxTimelineIndex()
+ /// \sa Room::syncEdge
+ QHash<StateEventKey, const StateEventBase*> currentState;
Timeline timeline;
PendingEvents unsyncedEvents;
QHash<QString, TimelineItem::index_t> eventsIndex;
- QString id;
- QStringList aliases;
- QString canonicalAlias;
- QString name;
QString displayname;
- QString topic;
- QString encryptionAlgorithm;
Avatar avatar;
- JoinState joinState;
int highlightCount = 0;
int notificationCount = 0;
members_map_t membersMap;
QList<User*> usersTyping;
QMultiHash<QString, User*> eventIdReadUsers;
+ QList<User*> usersInvited;
QList<User*> membersLeft;
int unreadMessages = 0;
bool displayed = false;
@@ -120,18 +117,21 @@ class Room::Private
std::unordered_map<QString, EventPtr> accountData;
QString prevBatch;
QPointer<GetRoomEventsJob> eventsHistoryJob;
+ QPointer<GetMembersByRoomJob> allMembersJob;
struct FileTransferPrivateInfo
{
-#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST
FileTransferPrivateInfo() = default;
- FileTransferPrivateInfo(BaseJob* j, QString fileName)
- : job(j), localFileInfo(fileName)
+ FileTransferPrivateInfo(BaseJob* j, const QString& fileName,
+ bool isUploading = false)
+ : status(FileTransferInfo::Started), job(j)
+ , localFileInfo(fileName), isUpload(isUploading)
{ }
-#endif
+
+ FileTransferInfo::Status status = FileTransferInfo::None;
QPointer<BaseJob> job = nullptr;
QFileInfo localFileInfo { };
- FileTransferInfo::Status status = FileTransferInfo::Started;
+ bool isUpload = false;
qint64 progress = 0;
qint64 total = -1;
@@ -164,13 +164,37 @@ class Room::Private
const RoomMessageEvent* getEventWithFile(const QString& eventId) const;
QString fileNameToDownload(const RoomMessageEvent* event) const;
+ Changes setSummary(RoomSummary&& newSummary);
+
//void inviteUser(User* u); // We might get it at some point in time.
void insertMemberIntoMap(User* u);
- void renameMember(User* u, QString oldName);
+ void renameMember(User* u, const QString& oldName);
void removeMemberFromMap(const QString& username, User* u);
+ // This updates the room displayname field (which is the way a room
+ // should be shown in the room list); called whenever the list of
+ // members, the room name (m.room.name) or canonical alias change.
+ void updateDisplayname();
+ // This is used by updateDisplayname() but only calculates the new name
+ // without any updates.
+ QString calculateDisplayname() const;
+
+ /// A point in the timeline corresponding to baseState
+ rev_iter_t timelineBase() const { return q->findInTimeline(-1); }
+
void getPreviousContent(int limit = 10);
+ template <typename EventT>
+ const EventT* getCurrentState(const QString& stateKey = {}) const
+ {
+ static const EventT empty;
+ const auto* evt =
+ currentState.value({EventT::matrixTypeId(), stateKey}, &empty);
+ Q_ASSERT(evt->type() == EventT::typeId() &&
+ evt->matrixType() == EventT::matrixTypeId());
+ return static_cast<const EventT*>(evt);
+ }
+
bool isEventNotable(const TimelineItem& ti) const
{
return !ti->isRedacted() &&
@@ -178,7 +202,29 @@ class Room::Private
is<RoomMessageEvent>(*ti);
}
- void addNewMessageEvents(RoomEvents&& events);
+ template <typename EventArrayT>
+ Changes updateStateFrom(EventArrayT&& events)
+ {
+ Changes changes = NoChange;
+ if (!events.empty())
+ {
+ QElapsedTimer et; et.start();
+ for (auto&& eptr: events)
+ {
+ const auto& evt = *eptr;
+ Q_ASSERT(evt.isStateEvent());
+ // Update baseState afterwards to make sure that the old state
+ // is valid and usable inside processStateEvent
+ changes |= q->processStateEvent(evt);
+ baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr);
+ }
+ if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
+ qCDebug(PROFILER) << "*** Room::Private::updateStateFrom():"
+ << events.size() << "event(s)," << et;
+ }
+ return changes;
+ }
+ Changes addNewMessageEvents(RoomEvents&& events);
void addHistoricalMessageEvents(RoomEvents&& events);
/** Move events into the timeline
@@ -190,20 +236,22 @@ class Room::Private
* @param placement - position and direction of insertion: Older for
* historical messages, Newer for new ones
*/
- Timeline::difference_type moveEventsToTimeline(RoomEventsRange events,
- EventsPlacement placement);
+ Timeline::size_type moveEventsToTimeline(RoomEventsRange events,
+ EventsPlacement placement);
/**
* Remove events from the passed container that are already in the timeline
*/
void dropDuplicateEvents(RoomEvents& events) const;
- void setLastReadEvent(User* u, QString eventId);
+ Changes setLastReadEvent(User* u, QString eventId);
void updateUnreadCount(rev_iter_t from, rev_iter_t to);
- void promoteReadMarker(User* u, rev_iter_t newMarker,
- bool force = false);
+ Changes promoteReadMarker(User* u, rev_iter_t newMarker,
+ bool force = false);
- void markMessagesAsRead(rev_iter_t upToMarker);
+ Changes markMessagesAsRead(rev_iter_t upToMarker);
+
+ void getAllMembers();
QString sendEvent(RoomEventPtr&& event);
@@ -213,17 +261,23 @@ class Room::Private
return sendEvent(makeEvent<EventT>(std::forward<ArgTs>(eventArgs)...));
}
+ RoomEvent* addAsPending(RoomEventPtr&& event);
+
QString doSendEvent(const RoomEvent* pEvent);
- PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr);
- void onEventSendingFailure(const RoomEvent* pEvent,
- const QString& txnId, BaseJob* call = nullptr);
+ void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr);
template <typename EvT>
- auto requestSetState(const QString& stateKey, const EvT& event)
+ SetRoomStateWithKeyJob* requestSetState(const QString& stateKey,
+ const EvT& event)
{
- // TODO: Queue up state events sending (see #133).
- return connection->callApi<SetRoomStateWithKeyJob>(
+ if (q->successorId().isEmpty())
+ {
+ // TODO: Queue up state events sending (see #133).
+ return connection->callApi<SetRoomStateWithKeyJob>(
id, EvT::matrixTypeId(), stateKey, event.contentJson());
+ }
+ qCWarning(MAIN) << q << "has been upgraded, state won't be set";
+ return nullptr;
}
template <typename EvT>
@@ -246,8 +300,10 @@ class Room::Private
QJsonObject toJson() const;
private:
- QString calculateDisplayname() const;
- QString roomNameFromMemberNames(const QList<User*>& userlist) const;
+ using users_shortlist_t = std::array<User*, 3>;
+ template<typename ContT>
+ users_shortlist_t buildShortlist(const ContT& users) const;
+ users_shortlist_t buildShortlist(const QStringList& userIds) const;
bool isLocalUser(const User* u) const
{
@@ -262,9 +318,13 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState)
// See "Accessing the Public Class" section in
// https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/
d->q = this;
- connect(this, &Room::userAdded, this, &Room::memberListChanged);
- connect(this, &Room::userRemoved, this, &Room::memberListChanged);
- connect(this, &Room::memberRenamed, this, &Room::memberListChanged);
+ d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name
+ connectUntil(connection, &Connection::loadedRoomState, this,
+ [this] (Room* r) {
+ if (this == r)
+ emit baseStateLoaded();
+ return this == r; // loadedRoomState fires only once per room
+ });
qCDebug(MAIN) << "New" << toCString(initialJoinState) << "Room:" << id;
}
@@ -278,6 +338,28 @@ const QString& Room::id() const
return d->id;
}
+QString Room::version() const
+{
+ const auto v = d->getCurrentState<RoomCreateEvent>()->version();
+ return v.isEmpty() ? QStringLiteral("1") : v;
+}
+
+bool Room::isUnstable() const
+{
+ return !connection()->loadingCapabilities() &&
+ !connection()->stableRoomVersions().contains(version());
+}
+
+QString Room::predecessorId() const
+{
+ return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId;
+}
+
+QString Room::successorId() const
+{
+ return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId();
+}
+
const Room::Timeline& Room::messageEvents() const
{
return d->timeline;
@@ -290,17 +372,17 @@ const Room::PendingEvents& Room::pendingEvents() const
QString Room::name() const
{
- return d->name;
+ return d->getCurrentState<RoomNameEvent>()->name();
}
QStringList Room::aliases() const
{
- return d->aliases;
+ return d->getCurrentState<RoomAliasesEvent>()->aliases();
}
QString Room::canonicalAlias() const
{
- return d->canonicalAlias;
+ return d->getCurrentState<RoomCanonicalAliasEvent>()->alias();
}
QString Room::displayName() const
@@ -308,9 +390,14 @@ QString Room::displayName() const
return d->displayname;
}
+void Room::refreshDisplayName()
+{
+ d->updateDisplayname();
+}
+
QString Room::topic() const
{
- return d->topic;
+ return d->getCurrentState<RoomTopicEvent>()->topic();
}
QString Room::avatarMediaId() const
@@ -323,6 +410,11 @@ QUrl Room::avatarUrl() const
return d->avatar.url();
}
+const Avatar& Room::avatarObject() const
+{
+ return d->avatar;
+}
+
QImage Room::avatar(int dimension)
{
return avatar(dimension, dimension);
@@ -368,14 +460,15 @@ void Room::setJoinState(JoinState state)
d->joinState = state;
qCDebug(MAIN) << "Room" << id() << "changed state: "
<< int(oldState) << "->" << int(state);
+ emit changed(Change::JoinStateChange);
emit joinStateChanged(oldState, state);
}
-void Room::Private::setLastReadEvent(User* u, QString eventId)
+Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId)
{
auto& storedId = lastReadEventIds[u];
if (storedId == eventId)
- return;
+ return Change::NoChange;
eventIdReadUsers.remove(storedId, u);
eventIdReadUsers.insert(eventId, u);
swap(storedId, eventId);
@@ -386,7 +479,9 @@ void Room::Private::setLastReadEvent(User* u, QString eventId)
if (storedId != serverReadMarker)
connection->callApi<PostReadMarkersJob>(id, storedId);
emit q->readMarkerMoved(eventId, storedId);
+ return Change::ReadMarkerChange;
}
+ return Change::NoChange;
}
void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to)
@@ -429,14 +524,15 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to)
}
}
-void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force)
+Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker,
+ bool force)
{
Q_ASSERT_X(u, __FUNCTION__, "User* should not be nullptr");
Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend());
const auto prevMarker = q->readMarker(u);
if (!force && prevMarker <= newMarker) // Remember, we deal with reverse iterators
- return;
+ return Change::NoChange;
Q_ASSERT(newMarker < timeline.crend());
@@ -445,13 +541,13 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force)
auto eagerMarker = find_if(newMarker.base(), timeline.cend(),
[=](const TimelineItem& ti) { return ti->senderId() != u->id(); });
- setLastReadEvent(u, (*(eagerMarker - 1))->id());
+ auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id());
if (isLocalUser(u))
{
const auto oldUnreadCount = unreadMessages;
QElapsedTimer et; et.start();
- unreadMessages = count_if(eagerMarker, timeline.cend(),
- std::bind(&Room::Private::isEventNotable, this, _1));
+ unreadMessages = int(count_if(eagerMarker, timeline.cend(),
+ std::bind(&Room::Private::isEventNotable, this, _1)));
if (et.nsecsElapsed() > profilerMinNsecs() / 10)
qCDebug(PROFILER) << "Recounting unread messages took" << et;
@@ -469,14 +565,16 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force)
qCDebug(MAIN) << "Room" << displayname << "still has"
<< unreadMessages << "unread message(s)";
emit q->unreadMessagesChanged(q);
+ changes |= Change::UnreadNotifsChange;
}
}
+ return changes;
}
-void Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
+Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
{
const auto prevMarker = q->readMarker();
- promoteReadMarker(q->localUser(), upToMarker);
+ auto changes = promoteReadMarker(q->localUser(), upToMarker);
if (prevMarker != upToMarker)
qCDebug(MAIN) << "Marked messages as read until" << *q->readMarker();
@@ -487,11 +585,12 @@ void Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
{
if ((*upToMarker)->senderId() != q->localUser()->id())
{
- connection->callApi<PostReceiptJob>(id, "m.read",
- (*upToMarker)->id());
+ connection->callApi<PostReceiptJob>(id, QStringLiteral("m.read"),
+ QUrl::toPercentEncoding((*upToMarker)->id()));
break;
}
}
+ return changes;
}
void Room::markMessagesAsRead(QString uptoEventId)
@@ -505,6 +604,29 @@ void Room::markAllMessagesAsRead()
d->markMessagesAsRead(d->timeline.crbegin());
}
+bool Room::canSwitchVersions() const
+{
+ if (!successorId().isEmpty())
+ return false; // Noone can upgrade a room that's already upgraded
+
+ // TODO, #276: m.room.power_levels
+ const auto* plEvt =
+ d->currentState.value({QStringLiteral("m.room.power_levels"), {}});
+ if (!plEvt)
+ return true;
+
+ const auto plJson = plEvt->contentJson();
+ const auto currentUserLevel =
+ plJson.value("users"_ls).toObject()
+ .value(localUser()->id()).toInt(
+ plJson.value("users_default"_ls).toInt());
+ const auto tombstonePowerLevel =
+ plJson.value("events"_ls).toObject()
+ .value("m.room.tombstone"_ls).toInt(
+ plJson.value("state_default"_ls).toInt());
+ return currentUserLevel >= tombstonePowerLevel;
+}
+
bool Room::hasUnreadMessages() const
{
return unreadCount() >= 0;
@@ -515,11 +637,21 @@ int Room::unreadCount() const
return d->unreadMessages;
}
-Room::rev_iter_t Room::timelineEdge() const
+Room::rev_iter_t Room::historyEdge() const
{
return d->timeline.crend();
}
+Room::Timeline::const_iterator Room::syncEdge() const
+{
+ return d->timeline.cend();
+}
+
+Room::rev_iter_t Room::timelineEdge() const
+{
+ return historyEdge();
+}
+
TimelineItem::index_t Room::minTimelineIndex() const
{
return d->timeline.empty() ? 0 : d->timeline.front().index();
@@ -554,6 +686,44 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const
return timelineEdge();
}
+Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId)
+{
+ return std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
+ [txnId] (const auto& item) { return item->transactionId() == txnId; });
+}
+
+Room::PendingEvents::const_iterator
+Room::findPendingEvent(const QString& txnId) const
+{
+ return std::find_if(d->unsyncedEvents.cbegin(), d->unsyncedEvents.cend(),
+ [txnId] (const auto& item) { return item->transactionId() == txnId; });
+}
+
+void Room::Private::getAllMembers()
+{
+ // If already loaded or already loading, there's nothing to do here.
+ if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob))
+ return;
+
+ allMembersJob = connection->callApi<GetMembersByRoomJob>(
+ id, connection->nextBatchToken(), "join");
+ auto nextIndex = timeline.empty() ? 0 : timeline.back().index() + 1;
+ connect( allMembersJob, &BaseJob::success, q, [=] {
+ Q_ASSERT(timeline.empty() || nextIndex <= q->maxTimelineIndex() + 1);
+ auto roomChanges = updateStateFrom(allMembersJob->chunk());
+ // Replay member events that arrived after the point for which
+ // the full members list was requested.
+ if (!timeline.empty() )
+ for (auto it = q->findInTimeline(nextIndex).base();
+ it != timeline.cend(); ++it)
+ if (is<RoomMemberEvent>(**it))
+ roomChanges |= q->processStateEvent(**it);
+ if (roomChanges&MembersChange)
+ emit q->memberListChanged();
+ emit q->allMembersLoaded();
+ });
+}
+
bool Room::displayed() const
{
return d->displayed;
@@ -570,6 +740,7 @@ void Room::setDisplayed(bool displayed)
{
resetHighlightCount();
resetNotificationCount();
+ d->getAllMembers();
}
}
@@ -669,6 +840,14 @@ void Room::resetHighlightCount()
emit highlightCountChanged(this);
}
+void Room::switchVersion(QString newVersion)
+{
+ auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion);
+ connect(job, &BaseJob::failure, this, [this,job] {
+ emit upgradeFailed(job->errorString());
+ });
+}
+
bool Room::hasAccountData(const QString& type) const
{
return d->accountData.find(type) != d->accountData.end();
@@ -768,7 +947,7 @@ void Room::Private::setTags(TagsMap newTags)
}
tags = move(newTags);
qCDebug(MAIN) << "Room" << q->objectName() << "is tagged with"
- << q->tagNames().join(", ");
+ << q->tagNames().join(QStringLiteral(", "));
emit q->tagsChanged();
}
@@ -839,7 +1018,7 @@ QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const
return fileName;
}
-QUrl Room::urlToThumbnail(const QString& eventId)
+QUrl Room::urlToThumbnail(const QString& eventId) const
{
if (auto* event = d->getEventWithFile(eventId))
if (event->hasThumbnail())
@@ -853,7 +1032,7 @@ QUrl Room::urlToThumbnail(const QString& eventId)
return {};
}
-QUrl Room::urlToDownload(const QString& eventId)
+QUrl Room::urlToDownload(const QString& eventId) const
{
if (auto* event = d->getEventWithFile(eventId))
{
@@ -865,7 +1044,7 @@ QUrl Room::urlToDownload(const QString& eventId)
return {};
}
-QString Room::fileNameToDownload(const QString& eventId)
+QString Room::fileNameToDownload(const QString& eventId) const
{
if (auto* event = d->getEventWithFile(eventId))
return d->fileNameToDownload(event);
@@ -890,7 +1069,7 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const
total = INT_MAX;
}
-#ifdef WORKAROUND_EXTENDED_INITIALIZER_LIST
+#ifdef BROKEN_INITIALIZER_LISTS
FileTransferInfo fti;
fti.status = infoIt->status;
fti.progress = int(progress);
@@ -899,13 +1078,28 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const
fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath());
return fti;
#else
- return { infoIt->status, int(progress), int(total),
+ return { infoIt->status, infoIt->isUpload, int(progress), int(total),
QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()),
QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath())
};
#endif
}
+QUrl Room::fileSource(const QString& id) const
+{
+ auto url = urlToDownload(id);
+ if (url.isValid())
+ return url;
+
+ // No urlToDownload means it's a pending or completed upload.
+ auto infoIt = d->fileTransfers.find(id);
+ if (infoIt != d->fileTransfers.end())
+ return QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath());
+
+ qCWarning(MAIN) << "File source for identifier" << id << "not found";
+ return {};
+}
+
QString Room::prettyPrint(const QString& plainText) const
{
return QMatrixClient::prettyPrint(plainText);
@@ -947,7 +1141,41 @@ int Room::timelineSize() const
bool Room::usesEncryption() const
{
- return !d->encryptionAlgorithm.isEmpty();
+ return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty();
+}
+
+int Room::joinedCount() const
+{
+ return d->summary.joinedMemberCount.omitted()
+ ? d->membersMap.size()
+ : d->summary.joinedMemberCount.value();
+}
+
+int Room::invitedCount() const
+{
+ // TODO: Store invited users in Room too
+ Q_ASSERT(!d->summary.invitedMemberCount.omitted());
+ return d->summary.invitedMemberCount.value();
+}
+
+int Room::totalMemberCount() const
+{
+ return joinedCount() + invitedCount();
+}
+
+GetRoomEventsJob* Room::eventsHistoryJob() const
+{
+ return d->eventsHistoryJob;
+}
+
+Room::Changes Room::Private::setSummary(RoomSummary&& newSummary)
+{
+ if (!summary.merge(newSummary))
+ return Change::NoChange;
+ qCDebug(MAIN).nospace().noquote()
+ << "Updated room summary for " << q->objectName() << ": " << summary;
+ emit q->memberListChanged();
+ return Change::SummaryChange;
}
void Room::Private::insertMemberIntoMap(User *u)
@@ -955,7 +1183,11 @@ void Room::Private::insertMemberIntoMap(User *u)
const auto userName = u->name(q);
// If there is exactly one namesake of the added user, signal member renaming
// for that other one because the two should be disambiguated now.
- auto namesakes = membersMap.values(userName);
+ const auto namesakes = membersMap.values(userName);
+
+ // Callers should check they are not adding an existing user once more.
+ Q_ASSERT(!namesakes.contains(u));
+
if (namesakes.size() == 1)
emit q->memberAboutToRename(namesakes.front(),
namesakes.front()->fullName(q));
@@ -964,7 +1196,7 @@ void Room::Private::insertMemberIntoMap(User *u)
emit q->memberRenamed(namesakes.front());
}
-void Room::Private::renameMember(User* u, QString oldName)
+void Room::Private::renameMember(User* u, const QString& oldName)
{
if (u->name(q) == oldName)
{
@@ -977,7 +1209,6 @@ void Room::Private::renameMember(User* u, QString oldName)
removeMemberFromMap(oldName, u);
insertMemberIntoMap(u);
}
- emit q->memberRenamed(u);
}
void Room::Private::removeMemberFromMap(const QString& username, User* u)
@@ -993,7 +1224,6 @@ void Room::Private::removeMemberFromMap(const QString& username, User* u)
membersMap.remove(username, u);
// If there was one namesake besides the removed user, signal member renaming
// for it because it doesn't need to be disambiguated anymore.
- // TODO: Think about left users.
if (namesake)
emit q->memberRenamed(namesake);
}
@@ -1003,13 +1233,14 @@ inline auto makeErrorStr(const Event& e, QByteArray msg)
return msg.append("; event dump follows:\n").append(e.originalJson());
}
-Room::Timeline::difference_type Room::Private::moveEventsToTimeline(
+Room::Timeline::size_type Room::Private::moveEventsToTimeline(
RoomEventsRange events, EventsPlacement placement)
{
Q_ASSERT(!events.empty());
// Historical messages arrive in newest-to-oldest order, so the process for
- // them is symmetric to the one for new messages.
- auto index = timeline.empty() ? -int(placement) :
+ // them is almost symmetric to the one for new messages. New messages get
+ // appended from index 0; old messages go backwards from index -1.
+ auto index = timeline.empty() ? -((placement+1)/2) /* 1 -> -1; -1 -> 0 */ :
placement == Older ? timeline.front().index() :
timeline.back().index();
auto baseIndex = index;
@@ -1030,7 +1261,7 @@ Room::Timeline::difference_type Room::Private::moveEventsToTimeline(
eventsIndex.insert(eId, index);
Q_ASSERT(q->findInTimeline(eId)->event()->id() == eId);
}
- const auto insertedSize = (index - baseIndex) * int(placement);
+ const auto insertedSize = (index - baseIndex) * placement;
Q_ASSERT(insertedSize == int(events.size()));
return insertedSize;
}
@@ -1076,50 +1307,40 @@ QString Room::roomMembername(const QString& userId) const
return roomMembername(user(userId));
}
-void Room::updateData(SyncRoomData&& data)
+void Room::updateData(SyncRoomData&& data, bool fromCache)
{
if( d->prevBatch.isEmpty() )
d->prevBatch = data.timelinePrevBatch;
setJoinState(data.joinState);
+ Changes roomChanges = Change::NoChange;
QElapsedTimer et; et.start();
for (auto&& event: data.accountData)
- processAccountDataEvent(move(event));
+ roomChanges |= processAccountDataEvent(move(event));
- bool emitNamesChanged = false;
- if (!data.state.empty())
- {
- et.restart();
- for (const auto& e: data.state)
- emitNamesChanged |= processStateEvent(*e);
+ roomChanges |= d->updateStateFrom(data.state);
- if (data.state.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::processStateEvents():"
- << data.state.size() << "event(s)," << et;
- }
if (!data.timeline.empty())
{
et.restart();
- // State changes can arrive in a timeline event; so check those.
- for (const auto& e: data.timeline)
- emitNamesChanged |= processStateEvent(*e);
+ roomChanges |= d->addNewMessageEvents(move(data.timeline));
if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):"
+ qCDebug(PROFILER) << "*** Room::addNewMessageEvents():"
<< data.timeline.size() << "event(s)," << et;
}
- if (emitNamesChanged)
+ if (roomChanges&TopicChange)
+ emit topicChanged();
+
+ if (roomChanges&NameChange)
emit namesChanged(this);
- d->updateDisplayname();
- if (!data.timeline.empty())
- {
- et.restart();
- d->addNewMessageEvents(move(data.timeline));
- if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs())
- qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et;
- }
+ if (roomChanges&MembersChange)
+ emit memberListChanged();
+
+ roomChanges |= d->setSummary(move(data.summary));
+
for( auto&& ephemeralEvent: data.ephemeral )
- processEphemeralEvent(move(ephemeralEvent));
+ roomChanges |= processEphemeralEvent(move(ephemeralEvent));
// See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count
if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages)
@@ -1139,29 +1360,45 @@ void Room::updateData(SyncRoomData&& data)
d->notificationCount = data.notificationCount;
emit notificationCountChanged(this);
}
+ if (roomChanges != Change::NoChange)
+ {
+ d->updateDisplayname();
+ emit changed(roomChanges);
+ if (!fromCache)
+ connection()->saveRoomState(this);
+ }
}
-QString Room::Private::sendEvent(RoomEventPtr&& event)
+RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event)
{
if (event->transactionId().isEmpty())
event->setTransactionId(connection->generateTxnId());
auto* pEvent = rawPtr(event);
- emit q->pendingEventAboutToAdd();
+ emit q->pendingEventAboutToAdd(pEvent);
unsyncedEvents.emplace_back(move(event));
emit q->pendingEventAdded();
- return doSendEvent(pEvent);
+ return pEvent;
+}
+
+QString Room::Private::sendEvent(RoomEventPtr&& event)
+{
+ if (q->successorId().isEmpty())
+ return doSendEvent(addAsPending(std::move(event)));
+
+ qCWarning(MAIN) << q << "has been upgraded, event won't be sent";
+ return {};
}
QString Room::Private::doSendEvent(const RoomEvent* pEvent)
{
- auto txnId = pEvent->transactionId();
+ const auto txnId = pEvent->transactionId();
// TODO, #133: Enqueue the job rather than immediately trigger it.
if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest,
id, pEvent->matrixType(), txnId, pEvent->contentJson()))
{
Room::connect(call, &BaseJob::started, q,
- [this,pEvent,txnId] {
- auto it = findAsPending(pEvent);
+ [this,txnId] {
+ auto it = q->findPendingEvent(txnId);
if (it == unsyncedEvents.end())
{
qWarning(EVENTS) << "Pending event for transaction" << txnId
@@ -1169,16 +1406,14 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
return;
}
it->setDeparted();
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
Room::connect(call, &BaseJob::failure, q,
- std::bind(&Room::Private::onEventSendingFailure,
- this, pEvent, txnId, call));
+ std::bind(&Room::Private::onEventSendingFailure, this, txnId, call));
Room::connect(call, &BaseJob::success, q,
- [this,call,pEvent,txnId] {
- // Find an event by the pointer saved in the lambda (the pointer
- // may be dangling by now but we can still search by it).
- auto it = findAsPending(pEvent);
+ [this,call,txnId] {
+ emit q->messageSent(txnId, call->eventId());
+ auto it = q->findPendingEvent(txnId);
if (it == unsyncedEvents.end())
{
qDebug(EVENTS) << "Pending event for transaction" << txnId
@@ -1187,26 +1422,16 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent)
}
it->setReachedServer(call->eventId());
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
});
} else
- onEventSendingFailure(pEvent, txnId);
+ onEventSendingFailure(txnId);
return txnId;
}
-Room::PendingEvents::iterator Room::Private::findAsPending(
- const RoomEvent* rawEvtPtr)
-{
- const auto comp =
- [rawEvtPtr] (const auto& pe) { return pe.event() == rawEvtPtr; };
-
- return std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), comp);
-}
-
-void Room::Private::onEventSendingFailure(const RoomEvent* pEvent,
- const QString& txnId, BaseJob* call)
+void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call)
{
- auto it = findAsPending(pEvent);
+ auto it = q->findPendingEvent(txnId);
if (it == unsyncedEvents.end())
{
qCritical(EVENTS) << "Pending event for transaction" << txnId
@@ -1216,15 +1441,40 @@ void Room::Private::onEventSendingFailure(const RoomEvent* pEvent,
it->setSendingFailed(call
? call->statusCaption() % ": " % call->errorString()
: tr("The call could not be started"));
- emit q->pendingEventChanged(it - unsyncedEvents.begin());
+ emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
}
QString Room::retryMessage(const QString& txnId)
{
- auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(),
- [txnId] (const auto& evt) { return evt->transactionId() == txnId; });
+ const auto it = findPendingEvent(txnId);
Q_ASSERT(it != d->unsyncedEvents.end());
qDebug(EVENTS) << "Retrying transaction" << txnId;
+ const auto& transferIt = d->fileTransfers.find(txnId);
+ if (transferIt != d->fileTransfers.end())
+ {
+ Q_ASSERT(transferIt->isUpload);
+ if (transferIt->status == FileTransferInfo::Completed)
+ {
+ qCDebug(MAIN) << "File for transaction" << txnId
+ << "has already been uploaded, bypassing re-upload";
+ } else {
+ if (isJobRunning(transferIt->job))
+ {
+ qCDebug(MAIN) << "Abandoning the upload job for transaction"
+ << txnId << "and starting again";
+ transferIt->job->abandon();
+ emit fileTransferFailed(txnId, tr("File upload will be retried"));
+ }
+ uploadFile(txnId,
+ QUrl::fromLocalFile(transferIt->localFileInfo.absoluteFilePath()));
+ // FIXME: Content type is no more passed here but it should
+ }
+ }
+ if (it->deliveryStatus() == EventStatus::ReachedServer)
+ {
+ qCWarning(MAIN) << "The previous attempt has reached the server; two"
+ " events are likely to be in the timeline after retry";
+ }
it->resetStatus();
return d->doSendEvent(it->event());
}
@@ -1235,7 +1485,22 @@ void Room::discardMessage(const QString& txnId)
[txnId] (const auto& evt) { return evt->transactionId() == txnId; });
Q_ASSERT(it != d->unsyncedEvents.end());
qDebug(EVENTS) << "Discarding transaction" << txnId;
- emit pendingEventAboutToDiscard(it - d->unsyncedEvents.begin());
+ const auto& transferIt = d->fileTransfers.find(txnId);
+ if (transferIt != d->fileTransfers.end())
+ {
+ Q_ASSERT(transferIt->isUpload);
+ if (isJobRunning(transferIt->job))
+ {
+ transferIt->status = FileTransferInfo::Cancelled;
+ transferIt->job->abandon();
+ emit fileTransferFailed(txnId, tr("File upload cancelled"));
+ } else if (transferIt->status == FileTransferInfo::Completed)
+ {
+ qCWarning(MAIN) << "File for transaction" << txnId
+ << "has been uploaded but the message was discarded";
+ }
+ }
+ emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin()));
d->unsyncedEvents.erase(it);
emit pendingEventDiscarded();
}
@@ -1251,7 +1516,7 @@ QString Room::postPlainText(const QString& plainText)
}
QString Room::postHtmlMessage(const QString& plainText, const QString& html,
- MessageEventType type)
+ MessageEventType type)
{
return d->sendEvent<RoomMessageEvent>(plainText, type,
new EventContent::TextContent(html, QStringLiteral("text/html")));
@@ -1259,7 +1524,65 @@ QString Room::postHtmlMessage(const QString& plainText, const QString& html,
QString Room::postHtmlText(const QString& plainText, const QString& html)
{
- return postHtmlMessage(plainText, html, MessageEventType::Text);
+ return postHtmlMessage(plainText, html);
+}
+
+QString Room::postFile(const QString& plainText, const QUrl& localPath,
+ bool asGenericFile)
+{
+ QFileInfo localFile { localPath.toLocalFile() };
+ Q_ASSERT(localFile.isFile());
+
+ const auto txnId = connection()->generateTxnId();
+ // Remote URL will only be known after upload; fill in the local path
+ // to enable the preview while the event is pending.
+ uploadFile(txnId, localPath);
+ {
+ auto&& event =
+ makeEvent<RoomMessageEvent>(plainText, localFile, asGenericFile);
+ event->setTransactionId(txnId);
+ d->addAsPending(std::move(event));
+ }
+ auto* context = new QObject(this);
+ connect(this, &Room::fileTransferCompleted, context,
+ [context,this,txnId] (const QString& id, QUrl, const QUrl& mxcUri) {
+ if (id == txnId)
+ {
+ auto it = findPendingEvent(txnId);
+ if (it != d->unsyncedEvents.end())
+ {
+ it->setFileUploaded(mxcUri);
+ emit pendingEventChanged(
+ int(it - d->unsyncedEvents.begin()));
+ d->doSendEvent(it->get());
+ } else {
+ // Normally in this situation we should instruct
+ // the media server to delete the file; alas, there's no
+ // API specced for that.
+ qCWarning(MAIN) << "File uploaded to" << mxcUri
+ << "but the event referring to it was cancelled";
+ }
+ context->deleteLater();
+ }
+ });
+ connect(this, &Room::fileTransferCancelled, this,
+ [context,this,txnId] (const QString& id) {
+ if (id == txnId)
+ {
+ auto it = findPendingEvent(txnId);
+ if (it != d->unsyncedEvents.end())
+ {
+ const auto idx = int(it - d->unsyncedEvents.begin());
+ emit pendingEventAboutToDiscard(idx);
+ // See #286 on why iterator may not be valid here.
+ d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx);
+ emit pendingEventDiscarded();
+ }
+ context->deleteLater();
+ }
+ });
+
+ return txnId;
}
QString Room::postEvent(RoomEvent* event)
@@ -1288,6 +1611,11 @@ void Room::setCanonicalAlias(const QString& newAlias)
d->requestSetState(RoomCanonicalAliasEvent(newAlias));
}
+void Room::setAliases(const QStringList& aliases)
+{
+ d->requestSetState(RoomAliasesEvent(aliases));
+}
+
void Room::setTopic(const QString& newTopic)
{
d->requestSetState(RoomTopicEvent(newTopic));
@@ -1315,7 +1643,24 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re)
bool Room::supportsCalls() const
{
- return d->membersMap.size() == 2;
+ return joinedCount() == 2;
+}
+
+void Room::checkVersion()
+{
+ const auto defaultVersion = connection()->defaultRoomVersion();
+ const auto stableVersions = connection()->stableRoomVersions();
+ Q_ASSERT(!defaultVersion.isEmpty());
+ // This method is only called after the base state has been loaded
+ // or the server capabilities have been loaded.
+ emit stabilityUpdated(defaultVersion, stableVersions);
+ if (!stableVersions.contains(version()))
+ {
+ qCDebug(MAIN) << this << "version is" << version()
+ << "which the server doesn't count as stable";
+ if (canSwitchVersions())
+ qCDebug(MAIN) << "The current user has enough privileges to fix it";
+ }
}
void Room::inviteCall(const QString& callId, const int lifetime,
@@ -1358,15 +1703,18 @@ void Room::getPreviousContent(int limit)
void Room::Private::getPreviousContent(int limit)
{
- if( !isJobRunning(eventsHistoryJob) )
- {
- eventsHistoryJob =
- connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit);
- connect( eventsHistoryJob, &BaseJob::success, q, [=] {
- prevBatch = eventsHistoryJob->end();
- addHistoricalMessageEvents(eventsHistoryJob->chunk());
- });
- }
+ if (isJobRunning(eventsHistoryJob))
+ return;
+
+ eventsHistoryJob =
+ connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit);
+ emit q->eventsHistoryJobChanged();
+ connect( eventsHistoryJob, &BaseJob::success, q, [=] {
+ prevBatch = eventsHistoryJob->end();
+ addHistoricalMessageEvents(eventsHistoryJob->chunk());
+ });
+ connect( eventsHistoryJob, &QObject::destroyed,
+ q, &Room::eventsHistoryJobChanged);
}
void Room::inviteToRoom(const QString& memberId)
@@ -1376,7 +1724,8 @@ void Room::inviteToRoom(const QString& memberId)
LeaveRoomJob* Room::leaveRoom()
{
- return connection()->callApi<LeaveRoomJob>(id());
+ // FIXME, #63: It should be RoomManager, not Connection
+ return connection()->leaveRoom(this);
}
SetRoomStateWithKeyJob*Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const
@@ -1401,8 +1750,8 @@ void Room::unban(const QString& userId)
void Room::redactEvent(const QString& eventId, const QString& reason)
{
- connection()->callApi<RedactEventJob>(
- id(), eventId, connection()->generateTxnId(), reason);
+ connection()->callApi<RedactEventJob>(id(),
+ QUrl::toPercentEncoding(eventId), connection()->generateTxnId(), reason);
}
void Room::uploadFile(const QString& id, const QUrl& localFilename,
@@ -1414,7 +1763,7 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename,
auto job = connection()->uploadFile(fileName, overrideContentType);
if (isJobRunning(job))
{
- d->fileTransfers.insert(id, { job, fileName });
+ d->fileTransfers.insert(id, { job, fileName, true });
connect(job, &BaseJob::uploadProgress, this,
[this,id] (qint64 sent, qint64 total) {
d->fileTransfers[id].update(sent, total);
@@ -1437,8 +1786,8 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
if (ongoingTransfer != d->fileTransfers.end() &&
ongoingTransfer->status == FileTransferInfo::Started)
{
- qCWarning(MAIN) << "Download for" << eventId
- << "already started; to restart, cancel it first";
+ qCWarning(MAIN) << "Transfer for" << eventId
+ << "is ongoing; download won't start";
return;
}
@@ -1452,13 +1801,21 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
Q_ASSERT(false);
return;
}
- const auto fileUrl = event->content()->fileInfo()->url;
+ const auto* const fileInfo = event->content()->fileInfo();
+ if (!fileInfo->isValid())
+ {
+ qCWarning(MAIN) << "Event" << eventId
+ << "has an empty or malformed mxc URL; won't download";
+ return;
+ }
+ const auto fileUrl = fileInfo->url;
auto filePath = localFilename.toLocalFile();
if (filePath.isEmpty())
{
// Build our own file path, starting with temp directory and eventId.
filePath = eventId;
- filePath = QDir::tempPath() % '/' % filePath.replace(':', '_') %
+ filePath = QDir::tempPath() % '/' %
+ filePath.replace(QRegularExpression("[/\\<>|\"*?:]"), "_") %
'#' % d->fileNameToDownload(event);
}
auto job = connection()->downloadFile(fileUrl, filePath);
@@ -1533,22 +1890,29 @@ RoomEventPtr makeRedacted(const RoomEvent& target,
const RedactionEvent& redaction)
{
auto originalJson = target.originalJsonObject();
- static const QStringList keepKeys =
- { EventIdKey, TypeKey, QStringLiteral("room_id"),
- QStringLiteral("sender"), QStringLiteral("state_key"),
- QStringLiteral("prev_content"), ContentKey,
- QStringLiteral("origin_server_ts") };
+ static const QStringList keepKeys {
+ EventIdKey, TypeKey, QStringLiteral("room_id"),
+ QStringLiteral("sender"), QStringLiteral("state_key"),
+ QStringLiteral("prev_content"), ContentKey,
+ QStringLiteral("hashes"), QStringLiteral("signatures"),
+ QStringLiteral("depth"), QStringLiteral("prev_events"),
+ QStringLiteral("prev_state"), QStringLiteral("auth_events"),
+ QStringLiteral("origin"), QStringLiteral("origin_server_ts"),
+ QStringLiteral("membership")
+ };
std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap
{ { RoomMemberEvent::typeId(), { QStringLiteral("membership") } }
-// , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }
+ , { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }
// , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } }
// , { RoomPowerLevels::typeId(),
// { QStringLiteral("ban"), QStringLiteral("events"),
// QStringLiteral("events_default"), QStringLiteral("kick"),
// QStringLiteral("redact"), QStringLiteral("state_default"),
// QStringLiteral("users"), QStringLiteral("users_default") } }
- , { RoomAliasesEvent::typeId(), { QStringLiteral("alias") } }
+ , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } }
+// , { RoomHistoryVisibility::typeId(),
+// { QStringLiteral("history_visibility") } }
};
for (auto it = originalJson.begin(); it != originalJson.end();)
{
@@ -1600,11 +1964,26 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
return true;
}
- // Make a new event from the redacted JSON, exchange events,
- // notify everyone and delete the old event
+ // Make a new event from the redacted JSON and put it in the timeline
+ // instead of the redacted one. oldEvent will be deleted on return.
auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction));
- q->onRedaction(*oldEvent, *ti.event());
qCDebug(MAIN) << "Redacted" << oldEvent->id() << "with" << redaction.id();
+ if (oldEvent->isStateEvent())
+ {
+ const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() };
+ Q_ASSERT(currentState.contains(evtKey));
+ if (currentState.value(evtKey) == oldEvent.get())
+ {
+ Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState
+ qCDebug(MAIN).nospace() << "Redacting state "
+ << oldEvent->matrixType() << "/" << oldEvent->stateKey();
+ // Retarget the current state to the newly made event.
+ if (q->processStateEvent(*ti))
+ emit q->namesChanged(q);
+ updateDisplayname();
+ }
+ }
+ q->onRedaction(*oldEvent, *ti);
emit q->replacedEvent(ti.event(), rawPtr(oldEvent));
return true;
}
@@ -1626,11 +2005,11 @@ inline bool isRedaction(const RoomEventPtr& ep)
return is<RedactionEvent>(*ep);
}
-void Room::Private::addNewMessageEvents(RoomEvents&& events)
+Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events)
{
dropDuplicateEvents(events);
if (events.empty())
- return;
+ return Change::NoChange;
// Pre-process redactions so that events that get redacted in the same
// batch landed in the timeline already redacted.
@@ -1655,8 +2034,17 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
// If the target event comes later, it comes already redacted.
}
+ // State changes arrive as a part of timeline; the current room state gets
+ // updated before merging events to the timeline because that's what
+ // clients historically expect. This may eventually change though if we
+ // postulate that the current state is only current between syncs but not
+ // within a sync.
+ Changes roomChanges = Change::NoChange;
+ for (const auto& eptr: events)
+ roomChanges |= q->processStateEvent(*eptr);
+
auto timelineSize = timeline.size();
- auto totalInserted = 0;
+ size_t totalInserted = 0;
for (auto it = events.begin(); it != events.end();)
{
auto nextPendingPair = findFirstOf(it, events.end(),
@@ -1677,12 +2065,22 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
break;
it = nextPending + 1;
- emit q->pendingEventAboutToMerge(nextPending->get(),
- nextPendingPair.second - unsyncedEvents.begin());
+ auto* nextPendingEvt = nextPending->get();
+ const auto pendingEvtIdx =
+ int(nextPendingPair.second - unsyncedEvents.begin());
+ emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx);
qDebug(EVENTS) << "Merging pending event from transaction"
- << (*nextPending)->transactionId() << "into"
- << (*nextPending)->id();
- unsyncedEvents.erase(nextPendingPair.second);
+ << nextPendingEvt->transactionId() << "into"
+ << nextPendingEvt->id();
+ auto transfer = fileTransfers.take(nextPendingEvt->transactionId());
+ if (transfer.status != FileTransferInfo::None)
+ fileTransfers.insert(nextPendingEvt->id(), transfer);
+ // After emitting pendingEventAboutToMerge() above we cannot rely
+ // on the previously obtained nextPendingPair.second staying valid
+ // because a signal handler may send another message, thereby altering
+ // unsyncedEvents (see #286). Fortunately, unsyncedEvents only grows at
+ // its back so we can rely on the index staying valid at least.
+ unsyncedEvents.erase(unsyncedEvents.begin() + pendingEvtIdx);
if (auto insertedSize = moveEventsToTimeline({nextPending, it}, Newer))
{
totalInserted += insertedSize;
@@ -1713,15 +2111,17 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events)
auto firstWriter = q->user((*from)->senderId());
if (q->readMarker(firstWriter) != timeline.crend())
{
- promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
+ roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1);
qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id()
<< "to" << *q->readMarker(firstWriter);
}
updateUnreadCount(timeline.crbegin(), rev_iter_t(from));
+ roomChanges |= Change::UnreadNotifsChange;
}
Q_ASSERT(timeline.size() == timelineSize + totalInserted);
+ return roomChanges;
}
void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
@@ -1730,14 +2130,25 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
const auto timelineSize = timeline.size();
dropDuplicateEvents(events);
- RoomEventsRange normalEvents {
- events.begin(), events.end() //remove_if(events.begin(), events.end(), isRedaction)
- };
- if (normalEvents.empty())
+ if (events.empty())
return;
- emit q->aboutToAddHistoricalMessages(normalEvents);
- const auto insertedSize = moveEventsToTimeline(normalEvents, Older);
+ // In case of lazy-loading new members may be loaded with historical
+ // messages. Also, the cache doesn't store events with empty content;
+ // so when such events show up in the timeline they should be properly
+ // incorporated.
+ for (const auto& eptr: events)
+ {
+ const auto& e = *eptr;
+ if (e.isStateEvent() &&
+ !currentState.contains({e.matrixType(), e.stateKey()}))
+ {
+ q->processStateEvent(e);
+ }
+ }
+
+ emit q->aboutToAddHistoricalMessages(events);
+ const auto insertedSize = moveEventsToTimeline(events, Older);
const auto from = timeline.crend() - insertedSize;
qCDebug(MAIN) << "Room" << displayname << "received" << insertedSize
@@ -1754,52 +2165,89 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events)
<< insertedSize << "event(s)," << et;
}
-bool Room::processStateEvent(const RoomEvent& e)
+Room::Changes Room::processStateEvent(const RoomEvent& e)
{
+ if (!e.isStateEvent())
+ return Change::NoChange;
+
+ const auto* oldStateEvent = std::exchange(
+ d->currentState[{e.matrixType(),e.stateKey()}],
+ static_cast<const StateEventBase*>(&e));
+ Q_ASSERT(!oldStateEvent ||
+ (oldStateEvent->matrixType() == e.matrixType() &&
+ oldStateEvent->stateKey() == e.stateKey()));
+ if (!is<RoomMemberEvent>(e)) // Room member events are too numerous
+ qCDebug(EVENTS) << "Room state event:" << e;
+
return visit(e
- , [this] (const RoomNameEvent& evt) {
- d->name = evt.name();
- qCDebug(MAIN) << "Room name updated:" << d->name;
- return true;
+ , [] (const RoomNameEvent&) {
+ return NameChange;
}
- , [this] (const RoomAliasesEvent& evt) {
- d->aliases = evt.aliases();
- qCDebug(MAIN) << "Room aliases updated:" << d->aliases;
- return true;
+ , [this,oldStateEvent] (const RoomAliasesEvent& ae) {
+ const auto previousAliases = oldStateEvent
+ ? static_cast<const RoomAliasesEvent*>(oldStateEvent)->aliases()
+ : QStringList();
+ connection()->updateRoomAliases(id(), previousAliases, ae.aliases());
+ return OtherChange;
}
, [this] (const RoomCanonicalAliasEvent& evt) {
- d->canonicalAlias = evt.alias();
- if (!d->canonicalAlias.isEmpty())
- setObjectName(d->canonicalAlias);
- qCDebug(MAIN) << "Room canonical alias updated:"
- << d->canonicalAlias;
- return true;
+ setObjectName(evt.alias().isEmpty() ? d->id : evt.alias());
+ return CanonicalAliasChange;
}
- , [this] (const RoomTopicEvent& evt) {
- d->topic = evt.topic();
- qCDebug(MAIN) << "Room topic updated:" << d->topic;
- emit topicChanged();
- return false;
+ , [] (const RoomTopicEvent&) {
+ return TopicChange;
}
, [this] (const RoomAvatarEvent& evt) {
if (d->avatar.updateUrl(evt.url()))
- {
- qCDebug(MAIN) << "Room avatar URL updated:"
- << evt.url().toString();
emit avatarChanged();
- }
- return false;
+ return AvatarChange;
}
- , [this] (const RoomMemberEvent& evt) {
+ , [this,oldStateEvent] (const RoomMemberEvent& evt) {
auto* u = user(evt.userId());
- u->processEvent(evt, this);
- if (u == localUser() && memberJoinState(u) == JoinState::Invite
+ const auto* oldMemberEvent =
+ static_cast<const RoomMemberEvent*>(oldStateEvent);
+ u->processEvent(evt, this, oldMemberEvent == nullptr);
+ const auto prevMembership = oldMemberEvent
+ ? oldMemberEvent->membership() : MembershipType::Leave;
+ if (u == localUser() && evt.membership() == MembershipType::Invite
&& evt.isDirect())
connection()->addToDirectChats(this, user(evt.senderId()));
- if( evt.membership() == MembershipType::Join )
+ switch (prevMembership)
{
- if (memberJoinState(u) != JoinState::Join)
+ case MembershipType::Invite:
+ if (evt.membership() != prevMembership)
+ {
+ d->usersInvited.removeOne(u);
+ Q_ASSERT(!d->usersInvited.contains(u));
+ }
+ break;
+ case MembershipType::Join:
+ if (evt.membership() == MembershipType::Invite)
+ qCWarning(MAIN)
+ << "Invalid membership change from Join to Invite:"
+ << evt;
+ if (evt.membership() != prevMembership)
+ {
+ disconnect(u, &User::nameAboutToChange, this, nullptr);
+ disconnect(u, &User::nameChanged, this, nullptr);
+ d->removeMemberFromMap(u->name(this), u);
+ emit userRemoved(u);
+ }
+ break;
+ default:
+ if (evt.membership() == MembershipType::Invite
+ || evt.membership() == MembershipType::Join)
+ {
+ d->membersLeft.removeOne(u);
+ Q_ASSERT(!d->membersLeft.contains(u));
+ }
+ }
+
+ switch(evt.membership())
+ {
+ case MembershipType::Join:
+ if (prevMembership != MembershipType::Join)
{
d->insertMemberIntoMap(u);
connect(u, &User::nameAboutToChange, this,
@@ -1810,35 +2258,50 @@ bool Room::processStateEvent(const RoomEvent& e)
connect(u, &User::nameChanged, this,
[=] (QString, QString oldName, const Room* context) {
if (context == this)
+ {
d->renameMember(u, oldName);
+ emit memberRenamed(u);
+ }
});
emit userAdded(u);
}
+ break;
+ case MembershipType::Invite:
+ if (!d->usersInvited.contains(u))
+ d->usersInvited.push_back(u);
+ break;
+ default:
+ if (!d->membersLeft.contains(u))
+ d->membersLeft.append(u);
}
- else if( evt.membership() == MembershipType::Leave )
- {
- if (memberJoinState(u) == JoinState::Join)
- {
- if (!d->membersLeft.contains(u))
- d->membersLeft.append(u);
- d->removeMemberFromMap(u->name(this), u);
- emit userRemoved(u);
- }
- }
- return false;
+ return MembersChange;
}
- , [this] (const EncryptionEvent& evt) {
- d->encryptionAlgorithm = evt.algorithm();
- qCDebug(MAIN) << "Encryption switched on in room" << id()
- << "with algorithm" << d->encryptionAlgorithm;
- emit encryption();
- return false;
+ , [this] (const EncryptionEvent&) {
+ emit encryption(); // It can only be done once, so emit it here.
+ return OtherChange;
+ }
+ , [this] (const RoomTombstoneEvent& evt) {
+ const auto successorId = evt.successorRoomId();
+ if (auto* successor = connection()->room(successorId))
+ emit upgraded(evt.serverMessage(), successor);
+ else
+ connectUntil(connection(), &Connection::loadedRoomState, this,
+ [this,successorId,serverMsg=evt.serverMessage()]
+ (Room* newRoom) {
+ if (newRoom->id() != successorId)
+ return false;
+ emit upgraded(serverMsg, newRoom);
+ return true;
+ });
+
+ return OtherChange;
}
);
}
-void Room::processEphemeralEvent(EventPtr&& event)
+Room::Changes Room::processEphemeralEvent(EventPtr&& event)
{
+ Changes changes = NoChange;
QElapsedTimer et; et.start();
if (auto* evt = eventCast<TypingEvent>(event))
{
@@ -1877,7 +2340,7 @@ void Room::processEphemeralEvent(EventPtr&& event)
continue; // FIXME, #185
auto u = user(r.userId);
if (memberJoinState(u) == JoinState::Join)
- d->promoteReadMarker(u, newMarker);
+ changes |= d->promoteReadMarker(u, newMarker);
}
} else
{
@@ -1894,7 +2357,7 @@ void Room::processEphemeralEvent(EventPtr&& event)
auto u = user(r.userId);
if (memberJoinState(u) == JoinState::Join &&
readMarker(u) == timelineEdge())
- d->setLastReadEvent(u, p.evtId);
+ changes |= d->setLastReadEvent(u, p.evtId);
}
}
}
@@ -1904,12 +2367,17 @@ void Room::processEphemeralEvent(EventPtr&& event)
<< evt->eventsWithReceipts().size()
<< "event(s) with" << totalReceipts << "receipt(s)," << et;
}
+ return changes;
}
-void Room::processAccountDataEvent(EventPtr&& event)
+Room::Changes Room::processAccountDataEvent(EventPtr&& event)
{
+ Changes changes = NoChange;
if (auto* evt = eventCast<TagEvent>(event))
+ {
d->setTags(evt->tags());
+ changes |= Change::TagsChange;
+ }
if (auto* evt = eventCast<ReadMarkerEvent>(event))
{
@@ -1917,10 +2385,9 @@ void Room::processAccountDataEvent(EventPtr&& event)
qCDebug(MAIN) << "Server-side read marker at" << readEventId;
d->serverReadMarker = readEventId;
const auto newMarker = findInTimeline(readEventId);
- if (newMarker != timelineEdge())
- d->markMessagesAsRead(newMarker);
- else
- d->setLastReadEvent(localUser(), readEventId);
+ changes |= newMarker != timelineEdge()
+ ? d->markMessagesAsRead(newMarker)
+ : d->setLastReadEvent(localUser(), readEventId);
}
// For all account data events
auto& currentData = d->accountData[event->matrixType()];
@@ -1933,52 +2400,40 @@ void Room::processAccountDataEvent(EventPtr&& event)
qCDebug(MAIN) << "Updated account data of type"
<< currentData->matrixType();
emit accountDataChanged(currentData->matrixType());
+ return Change::AccountDataChange;
}
+ return Change::NoChange;
}
-QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) const
+template <typename ContT>
+Room::Private::users_shortlist_t
+Room::Private::buildShortlist(const ContT& users) const
{
- // This is part 3(i,ii,iii) in the room displayname algorithm described
- // in the CS spec (see also Room::Private::updateDisplayname() ).
- // The spec requires to sort users lexicographically by state_key (user id)
- // and use disambiguated display names of two topmost users excluding
- // the current one to render the name of the room.
-
- // std::array is the leanest C++ container
- std::array<User*, 2> first_two = { {nullptr, nullptr} };
+ // To calculate room display name the spec requires to sort users
+ // lexicographically by state_key (user id) and use disambiguated
+ // display names of two topmost users excluding the current one to render
+ // the name of the room. The below code selects 3 topmost users,
+ // slightly extending the spec.
+ users_shortlist_t shortlist { }; // Prefill with nullptrs
std::partial_sort_copy(
- userlist.begin(), userlist.end(),
- first_two.begin(), first_two.end(),
- [this](const User* u1, const User* u2) {
- // Filter out the "me" user so that it never hits the room name
+ users.begin(), users.end(),
+ shortlist.begin(), shortlist.end(),
+ [this] (const User* u1, const User* u2) {
+ // localUser(), if it's in the list, is sorted below all others
return isLocalUser(u2) || (!isLocalUser(u1) && u1->id() < u2->id());
}
);
+ return shortlist;
+}
- // Spec extension. A single person in the chat but not the local user
- // (the local user is invited).
- if (userlist.size() == 1 && !isLocalUser(first_two.front()) &&
- joinState == JoinState::Invite)
- return tr("Invitation from %1")
- .arg(q->roomMembername(first_two.front()));
-
- // i. One-on-one chat. first_two[1] == localUser() in this case.
- if (userlist.size() == 2)
- return q->roomMembername(first_two[0]);
-
- // ii. Two users besides the current one.
- if (userlist.size() == 3)
- return tr("%1 and %2")
- .arg(q->roomMembername(first_two[0]),
- q->roomMembername(first_two[1]));
-
- // iii. More users.
- if (userlist.size() > 3)
- return tr("%1 and %Ln other(s)", "", userlist.size() - 3)
- .arg(q->roomMembername(first_two[0]));
-
- // userlist.size() < 2 - apparently, there's only current user in the room
- return QString();
+Room::Private::users_shortlist_t
+Room::Private::buildShortlist(const QStringList& userIds) const
+{
+ QList<User*> users;
+ users.reserve(userIds.size());
+ for (const auto& h: userIds)
+ users.push_back(q->user(h));
+ return buildShortlist(users);
}
QString Room::Private::calculateDisplayname() const
@@ -1987,27 +2442,73 @@ QString Room::Private::calculateDisplayname() const
// Numbers below refer to respective parts in the spec.
// 1. Name (from m.room.name)
- if (!name.isEmpty()) {
- return name;
+ auto dispName = q->name();
+ if (!dispName.isEmpty()) {
+ return dispName;
}
// 2. Canonical alias
- if (!canonicalAlias.isEmpty())
- return canonicalAlias;
+ dispName = q->canonicalAlias();
+ if (!dispName.isEmpty())
+ return dispName;
// Using m.room.aliases in naming is explicitly discouraged by the spec
- //if (!aliases.empty() && !aliases.at(0).isEmpty())
- // return aliases.at(0);
+
+ // Supplementary code for 3 and 4: build the shortlist of users whose names
+ // will be used to construct the room name. Takes into account MSC688's
+ // "heroes" if available.
+
+ const bool localUserIsIn = joinState == JoinState::Join;
+ const bool emptyRoom = membersMap.isEmpty() ||
+ (membersMap.size() == 1 && isLocalUser(*membersMap.begin()));
+ const bool nonEmptySummary =
+ !summary.heroes.omitted() && !summary.heroes->empty();
+ auto shortlist = nonEmptySummary ? buildShortlist(summary.heroes.value()) :
+ !emptyRoom ? buildShortlist(membersMap) :
+ users_shortlist_t { };
+
+ // When lazy-loading is on, we can rely on the heroes list.
+ // If it's off, the below code gathers invited and left members.
+ // NB: including invitations, if any, into naming is a spec extension.
+ // This kicks in when there's no lazy loading and it's a room with
+ // the local user as the only member, with more users invited.
+ if (!shortlist.front() && localUserIsIn)
+ shortlist = buildShortlist(usersInvited);
+
+ if (!shortlist.front()) // Still empty shortlist; use left members
+ shortlist = buildShortlist(membersLeft);
+
+ QStringList names;
+ for (auto u: shortlist)
+ {
+ if (u == nullptr || isLocalUser(u))
+ break;
+ // Only disambiguate if the room is not empty
+ names.push_back(u->displayname(emptyRoom ? nullptr : q));
+ }
+
+ const auto usersCountExceptLocal =
+ !emptyRoom ? q->joinedCount() - int(joinState == JoinState::Join) :
+ !usersInvited.empty() ? usersInvited.count() :
+ membersLeft.size() - int(joinState == JoinState::Leave);
+ if (usersCountExceptLocal > int(shortlist.size()))
+ names <<
+ tr("%Ln other(s)",
+ "Used to make a room name from user names: A, B and _N others_",
+ usersCountExceptLocal - int(shortlist.size()));
+ const auto namesList = QLocale().createSeparatedList(names);
// 3. Room members
- QString topMemberNames = roomNameFromMemberNames(membersMap.values());
- if (!topMemberNames.isEmpty())
- return topMemberNames;
+ if (!emptyRoom)
+ return namesList;
+
+ // (Spec extension) Invited users
+ if (!usersInvited.empty())
+ return tr("Empty room (invited: %1)").arg(namesList);
// 4. Users that previously left the room
- topMemberNames = roomNameFromMemberNames(membersLeft);
- if (!topMemberNames.isEmpty())
- return tr("Empty room (was: %1)").arg(topMemberNames);
+ if (membersLeft.size() > 0)
+ return tr("Empty room (was: %1)").arg(namesList);
// 5. Fail miserably
return tr("Empty room (%1)").arg(id);
@@ -2026,58 +2527,27 @@ void Room::Private::updateDisplayname()
}
}
-void appendStateEvent(QJsonArray& events, const QString& type,
- const QJsonObject& content, const QString& stateKey = {})
-{
- if (!content.isEmpty() || !stateKey.isEmpty())
- {
- auto json = basicEventJson(type, content);
- json.insert(QStringLiteral("state_key"), stateKey);
- events.append(json);
- }
-}
-
-#define ADD_STATE_EVENT(events, type, name, content) \
- appendStateEvent((events), QStringLiteral(type), \
- {{ QStringLiteral(name), content }});
-
-void appendEvent(QJsonArray& events, const QString& type,
- const QJsonObject& content)
-{
- if (!content.isEmpty())
- events.append(basicEventJson(type, content));
-}
-
-template <typename EvtT>
-void appendEvent(QJsonArray& events, const EvtT& event)
-{
- appendEvent(events, EvtT::matrixTypeId(), event.toJson());
-}
-
QJsonObject Room::Private::toJson() const
{
QElapsedTimer et; et.start();
QJsonObject result;
+ addParam<IfNotEmpty>(result, QStringLiteral("summary"), summary);
{
QJsonArray stateEvents;
- ADD_STATE_EVENT(stateEvents, "m.room.name", "name", name);
- ADD_STATE_EVENT(stateEvents, "m.room.topic", "topic", topic);
- ADD_STATE_EVENT(stateEvents, "m.room.avatar", "url",
- avatar.url().toString());
- ADD_STATE_EVENT(stateEvents, "m.room.aliases", "aliases",
- QJsonArray::fromStringList(aliases));
- ADD_STATE_EVENT(stateEvents, "m.room.canonical_alias", "alias",
- canonicalAlias);
- ADD_STATE_EVENT(stateEvents, "m.room.encryption", "algorithm",
- encryptionAlgorithm);
-
- for (const auto *m : membersMap)
- appendStateEvent(stateEvents, QStringLiteral("m.room.member"),
- { { QStringLiteral("membership"), QStringLiteral("join") }
- , { QStringLiteral("displayname"), m->rawName(q) }
- , { QStringLiteral("avatar_url"), m->avatarUrl(q).toString() }
- }, m->id());
+ for (const auto* evt: currentState)
+ {
+ Q_ASSERT(evt->isStateEvent());
+ if ((evt->isRedacted() && !is<RoomMemberEvent>(*evt)) ||
+ evt->contentJson().isEmpty())
+ continue;
+
+ auto json = evt->fullJson();
+ auto unsignedJson = evt->unsignedJson();
+ unsignedJson.remove(QStringLiteral("prev_content"));
+ json[UnsignedKeyL] = unsignedJson;
+ stateEvents.append(json);
+ }
const auto stateObjName = joinState == JoinState::Invite ?
QStringLiteral("invite_state") : QStringLiteral("state");
@@ -2085,14 +2555,17 @@ QJsonObject Room::Private::toJson() const
QJsonObject {{ QStringLiteral("events"), stateEvents }});
}
- QJsonArray accountDataEvents;
if (!accountData.empty())
{
+ QJsonArray accountDataEvents;
for (const auto& e: accountData)
- appendEvent(accountDataEvents, e.first, e.second->contentJson());
+ {
+ if (!e.second->contentJson().isEmpty())
+ accountDataEvents.append(e.second->fullJson());
+ }
+ result.insert(QStringLiteral("account_data"),
+ QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
}
- result.insert(QStringLiteral("account_data"),
- QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
QJsonObject unreadNotifObj
{ { SyncRoomData::UnreadCountKey, unreadMessages } };