diff options
Diffstat (limited to 'lib/room.cpp')
-rw-r--r-- | lib/room.cpp | 676 |
1 files changed, 374 insertions, 302 deletions
diff --git a/lib/room.cpp b/lib/room.cpp index c7c94fe5..ec2a34ef 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -21,6 +21,9 @@ #include "avatar.h" #include "connection.h" #include "converters.h" +#include "syncdata.h" +#include "user.h" + #include "csapi/account-data.h" #include "csapi/banning.h" #include "csapi/inviting.h" @@ -33,6 +36,7 @@ #include "csapi/room_upgrades.h" #include "csapi/rooms.h" #include "csapi/tags.h" + #include "events/callanswerevent.h" #include "events/callcandidatesevent.h" #include "events/callhangupevent.h" @@ -48,8 +52,6 @@ #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/postreadmarkersjob.h" -#include "syncdata.h" -#include "user.h" #include <QtCore/QDir> #include <QtCore/QHash> @@ -70,19 +72,25 @@ using std::move; using std::llround; #endif -enum EventsPlacement : int { Older = -1, Newer = 1 }; +enum EventsPlacement : int +{ + Older = -1, + Newer = 1 +}; class Room::Private { - public: - /** Map of user names to users. User names potentially duplicate, hence a - * multi-hashmap. */ +public: + /// Map of user names to users + /** User names potentially duplicate, hence QMultiHash. */ using members_map_t = QMultiHash<QString, User*>; Private(Connection* c, QString id_, JoinState initialJoinState) - : q(nullptr), connection(c), id(move(id_)), joinState(initialJoinState) - { - } + : q(nullptr) + , connection(c) + , id(move(id_)) + , joinState(initialJoinState) + {} Room* q; @@ -106,6 +114,7 @@ class Room::Private members_map_t membersMap; QList<User*> usersTyping; QMultiHash<QString, User*> eventIdReadUsers; + QList<User*> usersInvited; QList<User*> membersLeft; int unreadMessages = 0; bool displayed = false; @@ -119,16 +128,16 @@ class Room::Private QPointer<GetRoomEventsJob> eventsHistoryJob; QPointer<GetMembersByRoomJob> allMembersJob; - struct FileTransferPrivateInfo { + struct FileTransferPrivateInfo + { FileTransferPrivateInfo() = default; FileTransferPrivateInfo(BaseJob* j, const QString& fileName, bool isUploading = false) - : status(FileTransferInfo::Started), - job(j), - localFileInfo(fileName), - isUpload(isUploading) - { - } + : status(FileTransferInfo::Started) + , job(j) + , localFileInfo(fileName) + , isUpload(isUploading) + {} FileTransferInfo::Status status = FileTransferInfo::None; QPointer<BaseJob> job = nullptr; @@ -170,7 +179,7 @@ class Room::Private // 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 @@ -187,11 +196,11 @@ class Room::Private void getPreviousContent(int limit = 10); template <typename EventT> - const EventT* getCurrentState(QString stateKey = {}) const + const EventT* getCurrentState(const QString& stateKey = {}) const { static const EventT empty; - const auto* evt = currentState.value( - { EventT::matrixTypeId(), stateKey }, &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); @@ -200,7 +209,7 @@ class Room::Private bool isEventNotable(const TimelineItem& ti) const { return !ti->isRedacted() && ti->senderId() != connection->userId() - && is<RoomMessageEvent>(*ti); + && is<RoomMessageEvent>(*ti); } template <typename EventArrayT> @@ -219,8 +228,9 @@ class Room::Private baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); } if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::Private::updateStateFrom():" - << events.size() << "event(s)," << et; + qCDebug(PROFILER) + << "*** Room::Private::updateStateFrom():" << events.size() + << "event(s)," << et; } return changes; } @@ -236,8 +246,8 @@ 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 @@ -246,8 +256,7 @@ class Room::Private Changes setLastReadEvent(User* u, QString eventId); void updateUnreadCount(rev_iter_t from, rev_iter_t to); - Changes promoteReadMarker(User* u, rev_iter_t newMarker, - bool force = false); + Changes promoteReadMarker(User* u, rev_iter_t newMarker, bool force = false); Changes markMessagesAsRead(rev_iter_t upToMarker); @@ -273,13 +282,14 @@ class Room::Private if (q->successorId().isEmpty()) { // TODO: Queue up state events sending (see #133). return connection->callApi<SetRoomStateWithKeyJob>( - id, EvT::matrixTypeId(), stateKey, event.contentJson()); + id, EvT::matrixTypeId(), stateKey, event.contentJson()); } qCWarning(MAIN) << q << "has been upgraded, state won't be set"; return nullptr; } - template <typename EvT> auto requestSetState(const EvT& event) + template <typename EvT> + auto requestSetState(const EvT& event) { return connection->callApi<SetRoomStateJob>(id, EvT::matrixTypeId(), event.contentJson()); @@ -297,7 +307,7 @@ class Room::Private QJsonObject toJson() const; - private: +private: using users_shortlist_t = std::array<User*, 3>; template <typename ContT> users_shortlist_t buildShortlist(const ContT& users) const; @@ -307,19 +317,19 @@ class Room::Private }; Room::Room(Connection* connection, QString id, JoinState initialJoinState) - : QObject(connection), d(new Private(connection, id, initialJoinState)) + : QObject(connection) + , d(new Private(connection, id, initialJoinState)) { setObjectName(id); // See "Accessing the Public Class" section in // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; 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 - }); + 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; } @@ -330,13 +340,13 @@ const QString& Room::id() const { return d->id; } QString Room::version() const { const auto v = d->getCurrentState<RoomCreateEvent>()->version(); - return v.isEmpty() ? "1" : v; + return v.isEmpty() ? QStringLiteral("1") : v; } bool Room::isUnstable() const { return !connection()->loadingCapabilities() - && !connection()->stableRoomVersions().contains(version()); + && !connection()->stableRoomVersions().contains(version()); } QString Room::predecessorId() const @@ -356,6 +366,11 @@ const Room::PendingEvents& Room::pendingEvents() const return d->unsyncedEvents; } +bool Room::allHistoryLoaded() const +{ + return !d->timeline.empty() && is<RoomCreateEvent>(*d->timeline.front()); +} + QString Room::name() const { return d->getCurrentState<RoomNameEvent>()->name(); @@ -373,6 +388,8 @@ QString Room::canonicalAlias() const QString Room::displayName() const { return d->displayname; } +void Room::refreshDisplayName() { d->updateDisplayname(); } + QString Room::topic() const { return d->getCurrentState<RoomTopicEvent>()->topic(); @@ -396,8 +413,7 @@ QImage Room::avatar(int width, int height) const auto dcUsers = directChatUsers(); for (auto* u : dcUsers) if (u != localUser()) - return u->avatar(width, height, this, - [=] { emit avatarChanged(); }); + return u->avatar(width, height, this, [=] { emit avatarChanged(); }); return {}; } @@ -465,14 +481,13 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) QElapsedTimer et; et.start(); - const auto newUnreadMessages = count_if( - from, to, std::bind(&Room::Private::isEventNotable, this, _1)); + const auto newUnreadMessages = + count_if(from, to, std::bind(&Room::Private::isEventNotable, this, _1)); if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Counting gained unread messages took" << et; if (newUnreadMessages > 0) { - // See - // https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count + // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (unreadMessages < 0) unreadMessages = 0; @@ -480,8 +495,8 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) qCDebug(MAIN) << "Room" << q->objectName() << "has gained" << newUnreadMessages << "unread message(s)," << (q->readMarker() == timeline.crend() - ? "in total at least" - : "in total") + ? "in total at least" + : "in total") << unreadMessages << "unread message(s)"; emit q->unreadMessagesChanged(q); } @@ -494,17 +509,18 @@ Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, Q_ASSERT(newMarker >= timeline.crbegin() && newMarker <= timeline.crend()); const auto prevMarker = q->readMarker(u); - if (!force - && prevMarker <= newMarker) // Remember, we deal with reverse iterators + if (!force && prevMarker <= newMarker) // Remember, we deal with reverse + // iterators return Change::NoChange; Q_ASSERT(newMarker < timeline.crend()); // Try to auto-promote the read marker over the user's own messages // (switch to direct iterators for that). - auto eagerMarker = find_if( - newMarker.base(), timeline.cend(), - [=](const TimelineItem& ti) { return ti->senderId() != u->id(); }); + auto eagerMarker = find_if(newMarker.base(), timeline.cend(), + [=](const TimelineItem& ti) { + return ti->senderId() != u->id(); + }); auto changes = setLastReadEvent(u, (*(eagerMarker - 1))->id()); if (isLocalUser(u)) { @@ -512,20 +528,19 @@ Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, QElapsedTimer et; et.start(); unreadMessages = - count_if(eagerMarker, timeline.cend(), - std::bind(&Room::Private::isEventNotable, this, _1)); + 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; - // See - // https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count + // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (unreadMessages == 0) unreadMessages = -1; if (force || unreadMessages != oldUnreadCount) { if (unreadMessages == -1) { - qCDebug(MAIN) << "Room" << displayname - << "has no more unread messages"; + qCDebug(MAIN) + << "Room" << displayname << "has no more unread messages"; } else qCDebug(MAIN) << "Room" << displayname << "still has" << unreadMessages << "unread message(s)"; @@ -548,8 +563,9 @@ Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker) // until the previous last-read message, whichever comes first. for (; upToMarker < prevMarker; ++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; } } @@ -569,22 +585,26 @@ void Room::markAllMessagesAsRead() bool Room::canSwitchVersions() const { + if (!successorId().isEmpty()) + return false; // No one can upgrade a room that's already upgraded + // TODO, #276: m.room.power_levels - const auto* plEvt = d->currentState.value({ "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()); + plJson.value("users"_ls) + .toObject() + .value(localUser()->id()) + .toInt(plJson.value("users_default"_ls).toInt()); const auto tombstonePowerLevel = - plJson.value("events") - .toObject() - .value("m.room.tombstone"_ls) - .toInt(plJson.value("state_default"_ls).toInt()); + plJson.value("events"_ls) + .toObject() + .value("m.room.tombstone"_ls) + .toInt(plJson.value("state_default"_ls).toInt()); return currentUserLevel >= tombstonePowerLevel; } @@ -614,13 +634,13 @@ TimelineItem::index_t Room::maxTimelineIndex() const bool Room::isValidIndex(TimelineItem::index_t timelineIndex) const { return !d->timeline.empty() && timelineIndex >= minTimelineIndex() - && timelineIndex <= maxTimelineIndex(); + && timelineIndex <= maxTimelineIndex(); } Room::rev_iter_t Room::findInTimeline(TimelineItem::index_t index) const { return timelineEdge() - - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); + - (isValidIndex(index) ? index - minTimelineIndex() + 1 : 0); } Room::rev_iter_t Room::findInTimeline(const QString& evtId) const @@ -657,7 +677,7 @@ void Room::Private::getAllMembers() return; allMembersJob = connection->callApi<GetMembersByRoomJob>( - id, connection->nextBatchToken(), "join"); + 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); @@ -760,7 +780,7 @@ void Room::resetNotificationCount() if (d->notificationCount == 0) return; d->notificationCount = 0; - emit notificationCountChanged(this); + emit notificationCountChanged(); } int Room::highlightCount() const { return d->highlightCount; } @@ -770,14 +790,20 @@ void Room::resetHighlightCount() if (d->highlightCount == 0) return; d->highlightCount = 0; - emit highlightCountChanged(this); + emit highlightCountChanged(); } void Room::switchVersion(QString newVersion) { - auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion); - connect(job, &BaseJob::failure, this, - [this, job] { emit upgradeFailed(job->errorString()); }); + if (!successorId().isEmpty()) { + Q_ASSERT(!successorId().isEmpty()); + emit upgradeFailed(tr("The room is already upgraded")); + } + if (auto* job = connection()->callApi<UpgradeRoomJob>(id(), newVersion)) + connect(job, &BaseJob::failure, this, + [this, job] { emit upgradeFailed(job->errorString()); }); + else + emit upgradeFailed(tr("Couldn't initiate upgrade")); } bool Room::hasAccountData(const QString& type) const @@ -848,8 +874,8 @@ void Room::setTags(TagsMap newTags) { d->setTags(move(newTags)); connection()->callApi<SetAccountDataPerRoomJob>( - localUser()->id(), id(), TagEvent::matrixTypeId(), - TagEvent(d->tags).contentJson()); + localUser()->id(), id(), TagEvent::matrixTypeId(), + TagEvent(d->tags).contentJson()); } void Room::Private::setTags(TagsMap newTags) @@ -867,7 +893,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(); } @@ -980,8 +1006,7 @@ FileTransferInfo Room::fileTransferInfo(const QString& id) const fti.progress = int(progress); fti.total = int(total); fti.localDir = QUrl::fromLocalFile(infoIt->localFileInfo.absolutePath()); - fti.localPath = - QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); + fti.localPath = QUrl::fromLocalFile(infoIt->localFileInfo.absoluteFilePath()); return fti; #else return { infoIt->status, @@ -1040,8 +1065,8 @@ bool Room::usesEncryption() const int Room::joinedCount() const { return d->summary.joinedMemberCount.omitted() - ? d->membersMap.size() - : d->summary.joinedMemberCount.value(); + ? d->membersMap.size() + : d->summary.joinedMemberCount.value(); } int Room::invitedCount() const @@ -1059,8 +1084,8 @@ 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; + qCDebug(MAIN).nospace().noquote() + << "Updated room summary for " << q->objectName() << ": " << summary; emit q->memberListChanged(); return Change::SummaryChange; } @@ -1070,7 +1095,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)); @@ -1079,7 +1108,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) { qCWarning(MAIN) << "Room::Private::renameMember(): the user " @@ -1089,7 +1118,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) @@ -1097,15 +1125,13 @@ void Room::Private::removeMemberFromMap(const QString& username, User* u) User* namesake = nullptr; auto namesakes = membersMap.values(username); if (namesakes.size() == 2) { - namesake = - namesakes.front() == u ? namesakes.back() : namesakes.front(); + namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); emit q->memberAboutToRename(namesake, username); } 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. + // renaming for it because it doesn't need to be disambiguated any more. if (namesake) emit q->memberRenamed(namesake); } @@ -1115,7 +1141,7 @@ inline auto makeErrorStr(const Event& e, QByteArray msg) return msg.append("; event dump follows:\n").append(e.originalJson()); } -Room::Timeline::difference_type +Room::Timeline::size_type Room::Private::moveEventsToTimeline(RoomEventsRange events, EventsPlacement placement) { @@ -1124,21 +1150,19 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, // 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(); + ? -((placement + 1) / 2) /* 1 -> -1; -1 -> 0 */ + : placement == Older ? timeline.front().index() + : timeline.back().index(); auto baseIndex = index; for (auto&& e : events) { const auto eId = e->id(); Q_ASSERT_X(e, __FUNCTION__, "Attempt to add nullptr to timeline"); Q_ASSERT_X( - !eId.isEmpty(), __FUNCTION__, - makeErrorStr(*e, - "Event with empty id cannot be in the timeline")); + !eId.isEmpty(), __FUNCTION__, + makeErrorStr(*e, "Event with empty id cannot be in the timeline")); Q_ASSERT_X( - !eventsIndex.contains(eId), __FUNCTION__, - makeErrorStr(*e, - "Event is already in the timeline; " + !eventsIndex.contains(eId), __FUNCTION__, + makeErrorStr(*e, "Event is already in the timeline; " "incoming events were not properly deduplicated")); if (placement == Older) timeline.emplace_front(move(e), --index); @@ -1210,8 +1234,9 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) et.restart(); roomChanges |= d->addNewMessageEvents(move(data.timeline)); if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" - << data.timeline.size() << "event(s)," << et; + qCDebug(PROFILER) + << "*** Room::addNewMessageEvents():" << data.timeline.size() + << "event(s)," << et; } if (roomChanges & TopicChange) emit topicChanged(); @@ -1223,12 +1248,11 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) emit memberListChanged(); roomChanges |= d->setSummary(move(data.summary)); - d->updateDisplayname(); for (auto&& ephemeralEvent : data.ephemeral) roomChanges |= processEphemeralEvent(move(ephemeralEvent)); - // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count + // See https://github.com/quotient-im/libQuotient/wiki/unread_count if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) { qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount; d->unreadMessages = data.unreadCount; @@ -1237,13 +1261,14 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) if (data.highlightCount != d->highlightCount) { d->highlightCount = data.highlightCount; - emit highlightCountChanged(this); + emit highlightCountChanged(); } if (data.notificationCount != d->notificationCount) { d->notificationCount = data.notificationCount; - emit notificationCountChanged(this); + emit notificationCountChanged(); } if (roomChanges != Change::NoChange) { + d->updateDisplayname(); emit changed(roomChanges); if (!fromCache) connection()->saveRoomState(this); @@ -1275,8 +1300,8 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) 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())) { + BackgroundRequest, id, pEvent->matrixType(), txnId, + pEvent->contentJson())) { Room::connect(call, &BaseJob::started, q, [this, txnId] { auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { @@ -1285,7 +1310,7 @@ 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, @@ -1300,7 +1325,7 @@ 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(txnId); @@ -1315,10 +1340,9 @@ void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call) << "could not be sent"; return; } - it->setSendingFailed(call ? call->statusCaption() % ": " - % call->errorString() + 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) @@ -1340,16 +1364,15 @@ QString Room::retryMessage(const QString& txnId) emit fileTransferFailed(txnId, tr("File upload will be retried")); } - uploadFile(txnId, - QUrl::fromLocalFile( - transferIt->localFileInfo.absoluteFilePath())); + 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"; + << "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()); @@ -1357,9 +1380,10 @@ QString Room::retryMessage(const QString& txnId) void Room::discardMessage(const QString& txnId) { - auto it = std::find_if( - d->unsyncedEvents.begin(), d->unsyncedEvents.end(), - [txnId](const auto& evt) { return evt->transactionId() == txnId; }); + auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), + [txnId](const auto& evt) { + return evt->transactionId() == txnId; + }); Q_ASSERT(it != d->unsyncedEvents.end()); qDebug(EVENTS) << "Discarding transaction" << txnId; const auto& transferIt = d->fileTransfers.find(txnId); @@ -1371,8 +1395,8 @@ void Room::discardMessage(const QString& txnId) 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"; + << "File for transaction" << txnId + << "has been uploaded but the message was discarded"; } } emit pendingEventAboutToDiscard(int(it - d->unsyncedEvents.begin())); @@ -1394,8 +1418,8 @@ QString Room::postHtmlMessage(const QString& plainText, const QString& html, MessageEventType type) { return d->sendEvent<RoomMessageEvent>( - plainText, type, - new EventContent::TextContent(html, QStringLiteral("text/html"))); + plainText, type, + new EventContent::TextContent(html, QStringLiteral("text/html"))); } QString Room::postHtmlText(const QString& plainText, const QString& html) @@ -1408,31 +1432,34 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, { 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. - const auto txnId = - d->addAsPending(makeEvent<RoomMessageEvent>(plainText, localFile, - asGenericFile)) - ->transactionId(); 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) { + [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())); + 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"; + qCWarning(MAIN) + << "File uploaded to" << mxcUri + << "but the event referring to it was cancelled"; } context->deleteLater(); } @@ -1445,8 +1472,7 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, 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); + d->unsyncedEvents.erase(d->unsyncedEvents.begin() + idx); emit pendingEventDiscarded(); } context->deleteLater(); @@ -1470,7 +1496,7 @@ QString Room::postJson(const QString& matrixType, const QJsonObject& eventContent) { return d->sendEvent( - loadEvent<RoomEvent>(basicEventJson(matrixType, eventContent))); + loadEvent<RoomEvent>(basicEventJson(matrixType, eventContent))); } void Room::setName(const QString& newName) @@ -1519,7 +1545,7 @@ void Room::checkVersion() { const auto defaultVersion = connection()->defaultRoomVersion(); const auto stableVersions = connection()->stableRoomVersions(); - Q_ASSERT(!defaultVersion.isEmpty() && successorId().isEmpty()); + 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); @@ -1616,8 +1642,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, @@ -1660,19 +1686,24 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) const auto* event = d->getEventWithFile(eventId); if (!event) { qCCritical(MAIN) - << eventId - << "is not in the local timeline or has no file content"; + << eventId << "is not in the local timeline or has no file content"; 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(QRegularExpression("[/\\<>|\"*?:]"), "_") - % '#' % d->fileNameToDownload(event); + % filePath.replace(QRegularExpression("[/\\<>|\"*?:]"), "_") + % '#' % d->fileNameToDownload(event); } auto job = connection()->downloadFile(fileUrl, filePath); if (isJobRunning(job)) { @@ -1687,8 +1718,7 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) connect(job, &BaseJob::success, this, [this, eventId, fileUrl, job] { d->fileTransfers[eventId].status = FileTransferInfo::Completed; emit fileTransferCompleted( - eventId, fileUrl, - QUrl::fromLocalFile(job->targetFileName())); + eventId, fileUrl, QUrl::fromLocalFile(job->targetFileName())); }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, eventId, @@ -1718,10 +1748,10 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const // Multiple-remove (by different criteria), single-erase // 1. Check for duplicates against the timeline. - auto dupsBegin = - remove_if(events.begin(), events.end(), [&](const RoomEventPtr& e) { - return eventsIndex.contains(e->id()); - }); + auto dupsBegin = remove_if(events.begin(), events.end(), + [&](const RoomEventPtr& e) { + return eventsIndex.contains(e->id()); + }); // 2. Check for duplicates within the batch if there are still events. for (auto eIt = events.begin(); distance(eIt, dupsBegin) > 1; ++eIt) @@ -1771,9 +1801,8 @@ RoomEventPtr makeRedacted(const RoomEvent& target, // { QStringLiteral("ban"), QStringLiteral("events"), // QStringLiteral("events_default"), // QStringLiteral("kick"), QStringLiteral("redact"), - // QStringLiteral("state_default"), - // QStringLiteral("users"), QStringLiteral("users_default") - // } } + // QStringLiteral("state_default"), QStringLiteral("users"), + // QStringLiteral("users_default") } } , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } // , { RoomHistoryVisibility::typeId(), @@ -1785,9 +1814,9 @@ RoomEventPtr makeRedacted(const RoomEvent& target, else ++it; } - auto keepContentKeys = find_if( - keepContentKeysMap.begin(), keepContentKeysMap.end(), - [&target](const auto& t) { return target.type() == t.first; }); + auto keepContentKeys = + find_if(keepContentKeysMap.begin(), keepContentKeysMap.end(), + [&target](const auto& t) { return target.type() == t.first; }); if (keepContentKeys == keepContentKeysMap.end()) { originalJson.remove(ContentKeyL); originalJson.remove(PrevContentKeyL); @@ -1833,12 +1862,12 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() }; Q_ASSERT(currentState.contains(evtKey)); - if (currentState[evtKey] == oldEvent.get()) { - Q_ASSERT(ti.index() - >= 0); // Historical states can't be in currentState + if (currentState.value(evtKey) == oldEvent.get()) { + Q_ASSERT(ti.index() >= 0); // Historical states can't be in + // currentState qCDebug(MAIN).nospace() - << "Reverting state " << oldEvent->matrixType() << "/" - << oldEvent->stateKey(); + << "Redacting state " << oldEvent->matrixType() << "/" + << oldEvent->stateKey(); // Retarget the current state to the newly made event. if (q->processStateEvent(*ti)) emit q->namesChanged(q); @@ -1879,17 +1908,17 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // Try to find the target in the timeline, then in the batch. if (processRedaction(*r)) continue; - auto targetIt = std::find_if( - events.begin(), redactionIt, - [id = r->redactedEvent()](const RoomEventPtr& ep) { - return ep->id() == id; - }); + auto targetIt = + std::find_if(events.begin(), redactionIt, + [id = r->redactedEvent()](const RoomEventPtr& ep) { + return ep->id() == id; + }); if (targetIt != redactionIt) *targetIt = makeRedacted(**targetIt, *r); else qCDebug(MAIN) - << "Redaction" << r->id() << "ignored: target event" - << r->redactedEvent() << "is not found"; + << "Redaction" << r->id() << "ignored: target event" + << r->redactedEvent() << "is not found"; // If the target event comes later, it comes already redacted. } @@ -1903,11 +1932,11 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& 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(), unsyncedEvents.begin(), - unsyncedEvents.end(), isEchoEvent); + auto nextPendingPair = findFirstOf(it, events.end(), + unsyncedEvents.begin(), + unsyncedEvents.end(), isEchoEvent); auto nextPending = nextPendingPair.first; if (it != nextPending) { @@ -1926,7 +1955,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) it = nextPending + 1; auto* nextPendingEvt = nextPending->get(); const auto pendingEvtIdx = - int(nextPendingPair.second - unsyncedEvents.begin()); + int(nextPendingPair.second - unsyncedEvents.begin()); emit q->pendingEventAboutToMerge(nextPendingEvt, pendingEvtIdx); qDebug(EVENTS) << "Merging pending event from transaction" << nextPendingEvt->transactionId() << "into" @@ -1940,8 +1969,8 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) // 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)) { + if (auto insertedSize = moveEventsToTimeline({ nextPending, it }, + Newer)) { totalInserted += insertedSize; q->onAddNewTimelineEvents(timeline.cend() - insertedSize); } @@ -1956,9 +1985,8 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) emit q->callEvent(q, evt); if (totalInserted > 0) { - qCDebug(MAIN) << "Room" << q->objectName() << "received" - << totalInserted << "new events; the last event is now" - << timeline.back(); + qCDebug(MAIN) << "Room" << q->objectName() << "received" << totalInserted + << "new events; the last event is now" << timeline.back(); // The first event in the just-added batch (referred to by `from`) // defines whose read marker can possibly be promoted any further over @@ -1969,9 +1997,8 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) auto firstWriter = q->user((*from)->senderId()); if (q->readMarker(firstWriter) != timeline.crend()) { roomChanges |= promoteReadMarker(firstWriter, rev_iter_t(from) - 1); - qCDebug(MAIN) << "Auto-promoted read marker for" - << firstWriter->id() << "to" - << *q->readMarker(firstWriter); + qCDebug(MAIN) << "Auto-promoted read marker for" << firstWriter->id() + << "to" << *q->readMarker(firstWriter); } updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); @@ -2028,93 +2055,122 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return Change::NoChange; const auto* oldStateEvent = - std::exchange(d->currentState[{ e.matrixType(), e.stateKey() }], - static_cast<const StateEventBase*>(&e)); + 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)) + if (!is<RoomMemberEvent>(e)) // Room member events are too numerous qCDebug(EVENTS) << "Room state event:" << e; return visit( - e, [](const RoomNameEvent&) { return NameChange; }, - [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) { - setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); - return CanonicalAliasChange; - }, - [](const RoomTopicEvent&) { return TopicChange; }, - [this](const RoomAvatarEvent& evt) { - if (d->avatar.updateUrl(evt.url())) - emit avatarChanged(); - return AvatarChange; - }, - [this](const RoomMemberEvent& evt) { - auto* u = user(evt.userId()); - u->processEvent(evt, this); - if (u == localUser() && memberJoinState(u) == JoinState::Invite - && evt.isDirect()) - connection()->addToDirectChats(this, user(evt.senderId())); - - if (evt.membership() == MembershipType::Join) { - if (memberJoinState(u) != JoinState::Join) { - d->insertMemberIntoMap(u); - connect(u, &User::nameAboutToChange, this, - [=](QString newName, QString, - const Room* context) { - if (context == this) - emit memberAboutToRename(u, newName); - }); - connect(u, &User::nameChanged, this, - [=](QString, QString oldName, - const Room* context) { - if (context == this) - d->renameMember(u, oldName); - }); - emit userAdded(u); - } - } else if (evt.membership() != MembershipType::Join) { - if (memberJoinState(u) == JoinState::Join) { - if (evt.membership() == MembershipType::Invite) - qCWarning(MAIN) - << "Invalid membership change:" << evt; - if (!d->membersLeft.contains(u)) - d->membersLeft.append(u); - d->removeMemberFromMap(u->name(this), u); - emit userRemoved(u); - } + e, [](const RoomNameEvent&) { return NameChange; }, + [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) { + setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); + return CanonicalAliasChange; + }, + [](const RoomTopicEvent&) { return TopicChange; }, + [this](const RoomAvatarEvent& evt) { + if (d->avatar.updateUrl(evt.url())) + emit avatarChanged(); + return AvatarChange; + }, + [this, oldStateEvent](const RoomMemberEvent& evt) { + auto* u = user(evt.userId()); + 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())); + + switch (prevMembership) { + case MembershipType::Invite: + if (evt.membership() != prevMembership) { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); } - return MembersChange; - }, - [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; - }); + 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)); + } + } - return OtherChange; - }); + switch (evt.membership()) { + case MembershipType::Join: + if (prevMembership != MembershipType::Join) { + d->insertMemberIntoMap(u); + connect(u, &User::nameAboutToChange, this, + [=](QString newName, QString, const Room* context) { + if (context == this) + emit memberAboutToRename(u, newName); + }); + 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); + } + return MembersChange; + }, + [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; + }); } Room::Changes Room::processEphemeralEvent(EventPtr&& event) @@ -2175,9 +2231,9 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) - << "*** Room::processEphemeralEvent(receipts):" - << evt->eventsWithReceipts().size() << "event(s) with" - << totalReceipts << "receipt(s)," << et; + << "*** Room::processEphemeralEvent(receipts):" + << evt->eventsWithReceipts().size() << "event(s) with" + << totalReceipts << "receipt(s)," << et; } return changes; } @@ -2196,8 +2252,8 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) d->serverReadMarker = readEventId; const auto newMarker = findInTimeline(readEventId); changes |= newMarker != timelineEdge() - ? d->markMessagesAsRead(newMarker) - : d->setLastReadEvent(localUser(), readEventId); + ? d->markMessagesAsRead(newMarker) + : d->setLastReadEvent(localUser(), readEventId); } // For all account data events auto& currentData = d->accountData[event->matrixType()]; @@ -2225,12 +2281,11 @@ Room::Private::buildShortlist(const ContT& users) const // slightly extending the spec. users_shortlist_t shortlist {}; // Prefill with nullptrs std::partial_sort_copy( - 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()); - }); + 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; } @@ -2261,41 +2316,61 @@ QString Room::Private::calculateDisplayname() const return dispName; // Using m.room.aliases in naming is explicitly discouraged by the spec - // if (!q->aliases().empty() && !q->aliases().at(0).isEmpty()) - // return q->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 auto shortlist = !summary.heroes.omitted() - ? buildShortlist(summary.heroes.value()) - : !emptyRoom ? buildShortlist(membersMap) - : buildShortlist(membersLeft); + || (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; - names.push_back(q->roomMembername(u)); + // Only disambiguate if the room is not empty + names.push_back(u->displayname(emptyRoom ? nullptr : q)); } - auto usersCountExceptLocal = emptyRoom - ? membersLeft.size() - int(joinState == JoinState::Leave) - : q->joinedCount() - int(joinState == JoinState::Join); + 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); - auto namesList = QLocale().createSeparatedList(names); + "%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 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 if (membersLeft.size() > 0) return tr("Empty room (was: %1)").arg(namesList); @@ -2339,11 +2414,10 @@ QJsonObject Room::Private::toJson() const } const auto stateObjName = joinState == JoinState::Invite - ? QStringLiteral("invite_state") - : QStringLiteral("state"); - result.insert( - stateObjName, - QJsonObject { { QStringLiteral("events"), stateEvents } }); + ? QStringLiteral("invite_state") + : QStringLiteral("state"); + result.insert(stateObjName, + QJsonObject { { QStringLiteral("events"), stateEvents } }); } if (!accountData.empty()) { @@ -2353,16 +2427,15 @@ QJsonObject Room::Private::toJson() const accountDataEvents.append(e.second->fullJson()); } result.insert(QStringLiteral("account_data"), - QJsonObject { { QStringLiteral("events"), - accountDataEvents } }); + QJsonObject { + { QStringLiteral("events"), accountDataEvents } }); } QJsonObject unreadNotifObj { { SyncRoomData::UnreadCountKey, unreadMessages } }; if (highlightCount > 0) - unreadNotifObj.insert(QStringLiteral("highlight_count"), - highlightCount); + unreadNotifObj.insert(QStringLiteral("highlight_count"), highlightCount); if (notificationCount > 0) unreadNotifObj.insert(QStringLiteral("notification_count"), notificationCount); @@ -2370,8 +2443,7 @@ QJsonObject Room::Private::toJson() const result.insert(QStringLiteral("unread_notifications"), unreadNotifObj); if (et.elapsed() > 30) - qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" - << et; + qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et; return result; } |