diff options
Diffstat (limited to 'lib/room.cpp')
-rw-r--r-- | lib/room.cpp | 498 |
1 files changed, 276 insertions, 222 deletions
diff --git a/lib/room.cpp b/lib/room.cpp index 7c45bf89..8b81bfb2 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -25,7 +25,6 @@ #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/tags.h" @@ -46,10 +45,10 @@ #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> @@ -92,18 +91,19 @@ class Room::Private void updateDisplayname(); Connection* connection; + QString id; + JoinState joinState; + /// 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; @@ -157,8 +157,8 @@ class Room::Private fileTransfers[tid].status = FileTransferInfo::Failed; emit q->fileTransferFailed(tid, errorMessage); } - // A map from event/txn ids to information about the long operation; - // used for both download and upload operations + /// A map from event/txn ids to information about the long operation; + /// used for both download and upload operations QHash<QString, FileTransferPrivateInfo> fileTransfers; const RoomMessageEvent* getEventWithFile(const QString& eventId) const; @@ -169,8 +169,22 @@ class Room::Private void renameMember(User* u, QString oldName); void removeMemberFromMap(const QString& username, User* u); + /// 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(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 +192,7 @@ class Room::Private is<RoomMessageEvent>(*ti); } - void addNewMessageEvents(RoomEvents&& events); + Changes addNewMessageEvents(RoomEvents&& events); void addHistoricalMessageEvents(RoomEvents&& events); /** Move events into the timeline @@ -215,6 +229,8 @@ class Room::Private QString doSendEvent(const RoomEvent* pEvent); PendingEvents::iterator findAsPending(const RoomEvent* rawEvtPtr); + void onEventSendingFailure(const RoomEvent* pEvent, + const QString& txnId, BaseJob* call = nullptr); template <typename EvT> auto requestSetState(const QString& stateKey, const EvT& event) @@ -288,17 +304,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,7 +324,7 @@ QString Room::displayName() const QString Room::topic() const { - return d->topic; + return d->getCurrentState<RoomTopicEvent>()->topic(); } QString Room::avatarMediaId() const @@ -366,6 +382,7 @@ 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); } @@ -384,6 +401,7 @@ void Room::Private::setLastReadEvent(User* u, QString eventId) if (storedId != serverReadMarker) connection->callApi<PostReadMarkersJob>(id, storedId); emit q->readMarkerMoved(eventId, storedId); + connection->saveRoomState(q); } } @@ -408,7 +426,7 @@ 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)); - if (et.nsecsElapsed() > 10000) + if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Counting gained unread messages took" << et; if(newUnreadMessages > 0) @@ -418,7 +436,7 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) unreadMessages = 0; unreadMessages += newUnreadMessages; - qCDebug(MAIN) << "Room" << displayname << "has gained" + qCDebug(MAIN) << "Room" << q->objectName() << "has gained" << newUnreadMessages << "unread message(s)," << (q->readMarker() == timeline.crend() ? "in total at least" : "in total") @@ -450,7 +468,7 @@ void Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker, bool force) QElapsedTimer et; et.start(); unreadMessages = count_if(eagerMarker, timeline.cend(), std::bind(&Room::Private::isEventNotable, this, _1)); - if (et.nsecsElapsed() > 10000) + if (et.nsecsElapsed() > profilerMinNsecs() / 10) qCDebug(PROFILER) << "Recounting unread messages took" << et; // See https://github.com/QMatrixClient/libqmatrixclient/wiki/unread_count @@ -513,11 +531,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(); @@ -945,7 +973,12 @@ int Room::timelineSize() const bool Room::usesEncryption() const { - return !d->encryptionAlgorithm.isEmpty(); + return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty(); +} + +GetRoomEventsJob* Room::eventsHistoryJob() const +{ + return d->eventsHistoryJob; } void Room::Private::insertMemberIntoMap(User *u) @@ -1006,8 +1039,9 @@ Room::Timeline::difference_type Room::Private::moveEventsToTimeline( { 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; @@ -1028,7 +1062,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; } @@ -1074,45 +1108,48 @@ 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); + for (auto&& eptr: data.state) + { + const auto& evt = *eptr; + Q_ASSERT(evt.isStateEvent()); + d->baseState[{evt.matrixType(),evt.stateKey()}] = move(eptr); + roomChanges |= processStateEvent(evt); + } - qCDebug(PROFILER) << "*** Room::processStateEvents():" - << data.state.size() << "event(s)," << et; + 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); - qCDebug(PROFILER) << "*** Room::processStateEvents(timeline):" - << data.timeline.size() << "event(s)," << et; + roomChanges |= d->addNewMessageEvents(move(data.timeline)); + if (data.timeline.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + 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)); - qCDebug(PROFILER) << "*** Room::addNewMessageEvents():" << et; - } for( auto&& ephemeralEvent: data.ephemeral ) processEphemeralEvent(move(ephemeralEvent)); @@ -1134,6 +1171,12 @@ void Room::updateData(SyncRoomData&& data) d->notificationCount = data.notificationCount; emit notificationCountChanged(this); } + if (roomChanges != Change::NoChange) + { + emit changed(roomChanges); + if (!fromCache) + connection()->saveRoomState(this); + } } QString Room::Private::sendEvent(RoomEventPtr&& event) @@ -1150,50 +1193,42 @@ QString Room::Private::sendEvent(RoomEventPtr&& event) QString Room::Private::doSendEvent(const RoomEvent* pEvent) { auto txnId = pEvent->transactionId(); - // TODO: Enqueue the job rather than immediately trigger it - 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); - if (it == unsyncedEvents.end()) - { - qWarning(EVENTS) << "Pending event for transaction" << txnId - << "not found - got synced so soon?"; - return; - } - it->setDeparted(); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); - }); - Room::connect(call, &BaseJob::failure, q, - [this,pEvent,txnId,call] { - auto it = findAsPending(pEvent); - if (it == unsyncedEvents.end()) - { - qCritical(EVENTS) << "Pending event for transaction" << txnId - << "got lost without successful sending"; - return; - } - it->setSendingFailed( - call->statusCaption() % ": " % call->errorString()); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); - - }); - 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); - if (it == unsyncedEvents.end()) - { - qDebug(EVENTS) << "Pending event for transaction" << txnId - << "already merged"; - return; - } + // 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); + if (it == unsyncedEvents.end()) + { + qWarning(EVENTS) << "Pending event for transaction" << txnId + << "not found - got synced so soon?"; + return; + } + it->setDeparted(); + emit q->pendingEventChanged(it - unsyncedEvents.begin()); + }); + Room::connect(call, &BaseJob::failure, q, + std::bind(&Room::Private::onEventSendingFailure, + this, pEvent, 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); + if (it == unsyncedEvents.end()) + { + qDebug(EVENTS) << "Pending event for transaction" << txnId + << "already merged"; + return; + } - it->setReachedServer(call->eventId()); - emit q->pendingEventChanged(it - unsyncedEvents.begin()); - }); + it->setReachedServer(call->eventId()); + emit q->pendingEventChanged(it - unsyncedEvents.begin()); + }); + } else + onEventSendingFailure(pEvent, txnId); return txnId; } @@ -1206,6 +1241,22 @@ Room::PendingEvents::iterator Room::Private::findAsPending( return std::find_if(unsyncedEvents.begin(), unsyncedEvents.end(), comp); } +void Room::Private::onEventSendingFailure(const RoomEvent* pEvent, + const QString& txnId, BaseJob* call) +{ + auto it = findAsPending(pEvent); + if (it == unsyncedEvents.end()) + { + qCritical(EVENTS) << "Pending event for transaction" << txnId + << "could not be sent"; + return; + } + it->setSendingFailed(call + ? call->statusCaption() % ": " % call->errorString() + : tr("The call could not be started")); + emit q->pendingEventChanged(it - unsyncedEvents.begin()); +} + QString Room::retryMessage(const QString& txnId) { auto it = std::find_if(d->unsyncedEvents.begin(), d->unsyncedEvents.end(), @@ -1285,15 +1336,15 @@ bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) if (le->type() != re->type()) return false; - if (!le->id().isEmpty()) + if (!re->id().isEmpty()) return le->id() == re->id(); - if (!le->transactionId().isEmpty()) + if (!re->transactionId().isEmpty()) return le->transactionId() == re->transactionId(); // This one is not reliable (there can be two unsynced // events with the same type, sender and state key) but // it's the best we have for state events. - if (le->isStateEvent()) + if (re->isStateEvent()) return le->stateKey() == re->stateKey(); // Empty id and no state key, hmm... (shrug) @@ -1349,10 +1400,13 @@ void Room::Private::getPreviousContent(int limit) { 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); } } @@ -1587,11 +1641,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[evtKey] == oldEvent.get()) + { + Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState + qCDebug(MAIN).nospace() << "Reverting 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; } @@ -1613,48 +1682,49 @@ 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 Change::NoChange; // Pre-process redactions so that events that get redacted in the same // batch landed in the timeline already redacted. - // XXX: The code below is written (and commented) so that it could be - // quickly converted to not-saving redaction events in the timeline. - // See #220 for details. - auto newEnd = std::find_if(events.begin(), events.end(), isRedaction); - // Either process the redaction, or shift the non-redaction event - // overwriting redactions in a remove_if fashion. - for(const auto& eptr: RoomEventsRange(newEnd, events.end())) + // NB: We have to store redaction events to the timeline too - see #220. + auto redactionIt = std::find_if(events.begin(), events.end(), isRedaction); + for(const auto& eptr: RoomEventsRange(redactionIt, events.end())) if (auto* r = eventCast<RedactionEvent>(eptr)) { // Try to find the target in the timeline, then in the batch. if (processRedaction(*r)) continue; - auto targetIt = std::find_if(events.begin(), newEnd, + auto targetIt = std::find_if(events.begin(), redactionIt, [id=r->redactedEvent()] (const RoomEventPtr& ep) { return ep->id() == id; }); - if (targetIt != newEnd) + if (targetIt != redactionIt) *targetIt = makeRedacted(**targetIt, *r); else qCDebug(MAIN) << "Redaction" << r->id() << "ignored: target event" << r->redactedEvent() << "is not found"; - // If the target events comes later, it comes already redacted. + // If the target event comes later, it comes already redacted. } -// else // This should be uncommented once we stop adding redactions to the timeline -// *newEnd++ = std::move(eptr); - newEnd = events.end(); // This line should go if/when we stop adding redactions to the timeline - if (events.begin() == newEnd) - return; + // 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 stateChanges = Change::NoChange; + for (const auto& eptr: events) + stateChanges |= q->processStateEvent(*eptr); auto timelineSize = timeline.size(); auto totalInserted = 0; - for (auto it = events.begin(); it != newEnd;) + for (auto it = events.begin(); it != events.end();) { - auto nextPendingPair = findFirstOf(it, newEnd, + auto nextPendingPair = findFirstOf(it, events.end(), unsyncedEvents.begin(), unsyncedEvents.end(), isEchoEvent); auto nextPending = nextPendingPair.first; @@ -1668,7 +1738,7 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) q->onAddNewTimelineEvents(firstInserted); emit q->addedMessages(firstInserted->index(), timeline.back().index()); } - if (nextPending == newEnd) + if (nextPending == events.end()) break; it = nextPending + 1; @@ -1696,7 +1766,7 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) if (totalInserted > 0) { qCDebug(MAIN) - << "Room" << displayname << "received" << totalInserted + << "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`) @@ -1717,21 +1787,34 @@ void Room::Private::addNewMessageEvents(RoomEvents&& events) } Q_ASSERT(timeline.size() == timelineSize + totalInserted); + return stateChanges; } void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) { + QElapsedTimer et; et.start(); 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 @@ -1743,51 +1826,46 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) updateUnreadCount(from, timeline.crend()); Q_ASSERT(timeline.size() == timelineSize + insertedSize); + if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) + qCDebug(PROFILER) << "*** Room::addHistoricalMessageEvents():" + << insertedSize << "event(s)," << et; } -bool Room::processStateEvent(const RoomEvent& e) +Room::Changes Room::processStateEvent(const RoomEvent& e) { + if (!e.isStateEvent()) + return Change::NoChange; + + d->currentState[{e.matrixType(),e.stateKey()}] = + static_cast<const StateEventBase*>(&e); + if (!is<RoomMemberEvent>(e)) + 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; + , [] (const RoomAliasesEvent&) { + 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) { auto* u = user(evt.userId()); u->processEvent(evt, this); if (u == localUser() && memberJoinState(u) == JoinState::Invite && evt.isDirect()) - connection()->addToDirectChats(this, - user(evt.senderId())); + connection()->addToDirectChats(this, user(evt.senderId())); if( evt.membership() == MembershipType::Join ) { @@ -1807,24 +1885,23 @@ bool Room::processStateEvent(const RoomEvent& e) emit userAdded(u); } } - else if( evt.membership() == MembershipType::Leave ) + 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); } } - 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 EncryptionOn; } ); } @@ -1841,15 +1918,17 @@ void Room::processEphemeralEvent(EventPtr&& event) if (memberJoinState(u) == JoinState::Join) d->usersTyping.append(u); } - if (!evt->users().isEmpty()) + if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::processEphemeralEvent(typing):" << evt->users().size() << "users," << et; emit typingChanged(); } if (auto* evt = eventCast<ReceiptEvent>(event)) { + int totalReceipts = 0; for( const auto &p: qAsConst(evt->eventsWithReceipts()) ) { + totalReceipts += p.receipts.size(); { if (p.receipts.size() == 1) qCDebug(EPHEMERAL) << "Marking" << p.evtId @@ -1888,14 +1967,15 @@ void Room::processEphemeralEvent(EventPtr&& event) } } } - if (!evt->eventsWithReceipts().isEmpty()) + if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 || + et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) << "*** Room::processEphemeralEvent(receipts):" << evt->eventsWithReceipts().size() - << "events with receipts," << et; + << "event(s) with" << totalReceipts << "receipt(s)," << et; } } -void Room::processAccountDataEvent(EventPtr&& event) +Room::Changes Room::processAccountDataEvent(EventPtr&& event) { if (auto* evt = eventCast<TagEvent>(event)) d->setTags(evt->tags()); @@ -1922,7 +2002,9 @@ 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 @@ -1963,9 +2045,8 @@ QString Room::Private::roomNameFromMemberNames(const QList<User *> &userlist) co // iii. More users. if (userlist.size() > 3) - return tr("%1 and %L2 others") - .arg(q->roomMembername(first_two[0])) - .arg(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(); @@ -1977,27 +2058,29 @@ 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); + //if (!q->aliases().empty() && !q->aliases().at(0).isEmpty()) + // return q->aliases().at(0); // 3. Room members - QString topMemberNames = roomNameFromMemberNames(membersMap.values()); - if (!topMemberNames.isEmpty()) - return topMemberNames; + dispName = roomNameFromMemberNames(membersMap.values()); + if (!dispName.isEmpty()) + return dispName; // 4. Users that previously left the room - topMemberNames = roomNameFromMemberNames(membersLeft); - if (!topMemberNames.isEmpty()) - return tr("Empty room (was: %1)").arg(topMemberNames); + dispName = roomNameFromMemberNames(membersLeft); + if (!dispName.isEmpty()) + return tr("Empty room (was: %1)").arg(dispName); // 5. Fail miserably return tr("Empty room (%1)").arg(id); @@ -2016,34 +2099,6 @@ 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(); @@ -2051,23 +2106,19 @@ QJsonObject Room::Private::toJson() const { 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"); @@ -2075,14 +2126,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 } }; @@ -2094,7 +2148,7 @@ QJsonObject Room::Private::toJson() const result.insert(QStringLiteral("unread_notifications"), unreadNotifObj); - if (et.elapsed() > 50) + if (et.elapsed() > 30) qCDebug(PROFILER) << "Room::toJson() for" << displayname << "took" << et; return result; |