diff options
Diffstat (limited to 'lib/room.cpp')
-rw-r--r-- | lib/room.cpp | 298 |
1 files changed, 242 insertions, 56 deletions
diff --git a/lib/room.cpp b/lib/room.cpp index 9e7ff8d2..3cabe948 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -37,6 +37,7 @@ #include "events/roommemberevent.h" #include "events/typingevent.h" #include "events/receiptevent.h" +#include "events/reactionevent.h" #include "events/callinviteevent.h" #include "events/callcandidatesevent.h" #include "events/callanswerevent.h" @@ -98,6 +99,10 @@ class Room::Private Timeline timeline; PendingEvents unsyncedEvents; QHash<QString, TimelineItem::index_t> eventsIndex; + // A map from evtId to a map of relation type to a vector of event + // pointers. Not using QMultiHash, because we want to quickly return + // a number of relations for a given event without enumerating them. + QHash<QPair<QString, QString>, RelatedEvents> relations; QString displayname; Avatar avatar; int highlightCount = 0; @@ -183,6 +188,7 @@ class Room::Private rev_iter_t timelineBase() const { return q->findInTimeline(-1); } void getPreviousContent(int limit = 10); + bool allHistoryLoaded() const; template <typename EventT> const EventT* getCurrentState(const QString& stateKey = {}) const @@ -292,9 +298,18 @@ class Room::Private * * Tries to find an event in the timeline and redact it; deletes the * redaction event whether the redacted event was found or not. + * \return true if the event has been found and redacted; false otherwise */ bool processRedaction(const RedactionEvent& redaction); + /*! Apply a new revision of the event to the timeline + * + * Tries to find an event in the timeline and replace it with the new + * content passed in \p newMessage. + * \return true if the event has been found and replaced; false otherwise + */ + bool processReplacement(const RoomMessageEvent& newMessage); + void setTags(TagsMap newTags); QJsonObject toJson() const; @@ -370,6 +385,11 @@ const Room::PendingEvents& Room::pendingEvents() const return d->unsyncedEvents; } +bool Room::Private::allHistoryLoaded() const +{ + return !timeline.empty() && is<RoomCreateEvent>(*timeline.front()); +} + QString Room::name() const { return d->getCurrentState<RoomNameEvent>()->name(); @@ -377,7 +397,17 @@ QString Room::name() const QStringList Room::aliases() const { - return d->getCurrentState<RoomAliasesEvent>()->aliases(); + const auto* evt = d->getCurrentState<RoomCanonicalAliasEvent>(); + auto aliases = fromJson<QStringList>(evt->contentJson()["alt_aliases"]); + if (!evt->alias().isEmpty()) + aliases << evt->alias(); + return aliases; +} + +QStringList Room::altAliases() const +{ + const auto* evt = d->getCurrentState<RoomCanonicalAliasEvent>(); + return fromJson<QStringList>(evt->contentJson()["alt_aliases"]); } QString Room::canonicalAlias() const @@ -493,7 +523,9 @@ void Room::Private::updateUnreadCount(rev_iter_t from, rev_iter_t to) // that has just arrived. In this case we should recalculate // unreadMessages and might need to promote the read marker further // over local-origin messages. - const auto readMarker = q->readMarker(); + auto readMarker = q->readMarker(); + if (readMarker == timeline.crend() && allHistoryLoaded()) + --readMarker; // Read marker not found in the timeline, initialise it if (readMarker >= from && readMarker < to) { promoteReadMarker(q->localUser(), readMarker, true); @@ -680,10 +712,10 @@ Room::rev_iter_t Room::findInTimeline(const QString& evtId) const if (!d->timeline.empty() && d->eventsIndex.contains(evtId)) { auto it = findInTimeline(d->eventsIndex.value(evtId)); - Q_ASSERT((*it)->id() == evtId); + Q_ASSERT(it != historyEdge() && (*it)->id() == evtId); return it; } - return timelineEdge(); + return historyEdge(); } Room::PendingEvents::iterator Room::findPendingEvent(const QString& txnId) @@ -699,6 +731,18 @@ Room::findPendingEvent(const QString& txnId) const [txnId] (const auto& item) { return item->transactionId() == txnId; }); } +const Room::RelatedEvents Room::relatedEvents(const QString& evtId, + const char* relType) const +{ + return d->relations.value({ evtId, relType }); +} + +const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt, + const char* relType) const +{ + return relatedEvents(evt.id(), relType); +} + void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. @@ -961,6 +1005,11 @@ bool Room::isLowPriority() const return d->tags.contains(LowPriorityTag); } +bool Room::isServerNoticeRoom() const +{ + return d->tags.contains(ServerNoticeTag); +} + bool Room::isDirectChat() const { return connection()->isDirectChat(id()); @@ -971,6 +1020,11 @@ QList<User*> Room::directChatUsers() const return connection()->directChatUsers(this); } +QString safeFileName(QString rawName) +{ + return rawName.replace(QRegularExpression("[/\\<>|\"*?:]"), "_"); +} + const RoomMessageEvent* Room::Private::getEventWithFile(const QString& eventId) const { @@ -987,24 +1041,26 @@ Room::Private::getEventWithFile(const QString& eventId) const QString Room::Private::fileNameToDownload(const RoomMessageEvent* event) const { - Q_ASSERT(event->hasFileContent()); + Q_ASSERT(event && event->hasFileContent()); const auto* fileInfo = event->content()->fileInfo(); QString fileName; if (!fileInfo->originalName.isEmpty()) - { - fileName = QFileInfo(fileInfo->originalName).fileName(); - } - else if (!event->plainBody().isEmpty()) - { + fileName = QFileInfo(safeFileName(fileInfo->originalName)).fileName(); + else { // Having no better options, assume that the body has // the original file URL or at least the file name. QUrl u { event->plainBody() }; if (u.isValid()) - fileName = QFileInfo(u.path()).fileName(); + { + qDebug(MAIN) << event->id() + << "has no file name supplied but the event body " + "looks like a URL - using the file name from it"; + fileName = u.fileName(); + } } - // Check the file name for sanity - if (fileName.isEmpty() || !QTemporaryFile(fileName).open()) - return "file." % fileInfo->mimeType.preferredSuffix(); + if (fileName.isEmpty()) + return safeFileName(fileInfo->mediaId()).replace('.', '-') % '.' + % fileInfo->mimeType.preferredSuffix(); if (QSysInfo::productType() == "windows") { @@ -1331,7 +1387,7 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) if (roomChanges&TopicChange) emit topicChanged(); - if (roomChanges&NameChange) + if (roomChanges&(NameChange|CanonicalAliasChange)) emit namesChanged(this); if (roomChanges&MembersChange) @@ -1347,17 +1403,20 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) { qCDebug(MAIN) << "Setting unread_count to" << data.unreadCount; d->unreadMessages = data.unreadCount; + roomChanges |= Change::UnreadNotifsChange; emit unreadMessagesChanged(this); } if( data.highlightCount != d->highlightCount ) { d->highlightCount = data.highlightCount; + roomChanges |= Change::UnreadNotifsChange; emit highlightCountChanged(this); } if( data.notificationCount != d->notificationCount ) { d->notificationCount = data.notificationCount; + roomChanges |= Change::UnreadNotifsChange; emit notificationCountChanged(this); } if (roomChanges != Change::NoChange) @@ -1527,6 +1586,11 @@ QString Room::postHtmlText(const QString& plainText, const QString& html) return postHtmlMessage(plainText, html); } +QString Room::postReaction(const QString &eventId, const QString &key) +{ + return d->sendEvent<ReactionEvent>(EventRelation::annotate(eventId, key)); +} + QString Room::postFile(const QString& plainText, const QUrl& localPath, bool asGenericFile) { @@ -1608,12 +1672,18 @@ void Room::setName(const QString& newName) void Room::setCanonicalAlias(const QString& newAlias) { - d->requestSetState(RoomCanonicalAliasEvent(newAlias)); + connection()->callApi<SetRoomStateJob>( + id(), RoomCanonicalAliasEvent::matrixTypeId(), + QJsonObject { { "alias", newAlias }, + { "alt_aliases", QMatrixClient::toJson(altAliases()) } }); } void Room::setAliases(const QStringList& aliases) { - d->requestSetState(RoomAliasesEvent(aliases)); + connection()->callApi<SetRoomStateJob>( + id(), RoomCanonicalAliasEvent::matrixTypeId(), + QJsonObject { { "alias", canonicalAlias() }, + { "alt_aliases", QMatrixClient::toJson(aliases) } }); } void Room::setTopic(const QString& newTopic) @@ -1813,17 +1883,20 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) 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 = + fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event); + + if (filePath.size() > 200) // If too long, elide in the middle + filePath.replace(128, filePath.size() - 192, "---"); + + filePath = QDir::tempPath() % '/' % filePath; + qDebug(MAIN) << "File path:" << filePath; } auto job = connection()->downloadFile(fileUrl, filePath); if (isJobRunning(job)) { - // If there was a previous transfer (completed or failed), remove it. - d->fileTransfers.remove(eventId); - d->fileTransfers.insert(eventId, { job, job->targetFileName() }); + // If there was a previous transfer (completed or failed), overwrite it. + d->fileTransfers[eventId] = { job, job->targetFileName() }; connect(job, &BaseJob::downloadProgress, this, [this,eventId] (qint64 received, qint64 total) { d->fileTransfers[eventId].update(received, total); @@ -1893,7 +1966,6 @@ RoomEventPtr makeRedacted(const RoomEvent& target, 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"), @@ -1910,7 +1982,6 @@ RoomEventPtr makeRedacted(const RoomEvent& target, // QStringLiteral("events_default"), QStringLiteral("kick"), // QStringLiteral("redact"), QStringLiteral("state_default"), // QStringLiteral("users"), QStringLiteral("users_default") } } - , { RoomAliasesEvent::typeId(), { QStringLiteral("aliases") } } // , { RoomHistoryVisibility::typeId(), // { QStringLiteral("history_visibility") } } }; @@ -1983,11 +2054,65 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) updateDisplayname(); } } + if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) { + const auto& targetEvtId = reaction->relation().eventId; + const auto lookupKey = qMakePair(targetEvtId, + EventRelation::Annotation()); + if (relations.contains(lookupKey)) { + relations[lookupKey].removeOne(reaction); + } + } q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); return true; } +/** Make a replaced event + * + * Takes \p target and returns a copy of it with content taken from + * \p replacement. Disposal of the original event after that is on the caller. + */ +RoomEventPtr makeReplaced(const RoomEvent& target, + const RoomMessageEvent& replacement) +{ + auto originalJson = target.originalJsonObject(); + originalJson[ContentKeyL] = replacement.contentJson().value("m.new_content"_ls); + + auto unsignedData = originalJson.take(UnsignedKeyL).toObject(); + auto relations = unsignedData.take("m.relations"_ls).toObject(); + relations["m.replace"_ls] = replacement.id(); + unsignedData.insert(QStringLiteral("m.relations"), relations); + originalJson.insert(UnsignedKey, unsignedData); + + return loadEvent<RoomEvent>(originalJson); +} + +bool Room::Private::processReplacement(const RoomMessageEvent& newEvent) +{ + // Can't use findInTimeline because it returns a const iterator, and + // we need to change the underlying TimelineItem. + const auto pIdx = eventsIndex.find(newEvent.replacedEvent()); + if (pIdx == eventsIndex.end()) + return false; + + Q_ASSERT(q->isValidIndex(*pIdx)); + + auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())]; + if (ti->replacedBy() == newEvent.id()) + { + qCDebug(MAIN) << "Event" << ti->id() << "is already replaced with" + << newEvent.id(); + return true; + } + + // 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(makeReplaced(*ti, newEvent)); + qCDebug(MAIN) << "Replaced" << oldEvent->id() << "with" << newEvent.id(); + emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); + return true; +} + Connection* Room::connection() const { Q_ASSERT(d->connection); @@ -1999,10 +2124,16 @@ User* Room::localUser() const return connection()->user(); } -inline bool isRedaction(const RoomEventPtr& ep) +/// Whether the event is a redaction or a replacement +inline bool isEditing(const RoomEventPtr& ep) { Q_ASSERT(ep); - return is<RedactionEvent>(*ep); + if (is<RedactionEvent>(*ep)) + return true; + if (auto* msgEvent = eventCast<RoomMessageEvent>(ep)) + return msgEvent->replacedEvent().isEmpty(); + + return false; } Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) @@ -2011,28 +2142,52 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& 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. - // 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(), 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"; - // If the target event comes later, it comes already redacted. + { + // Pre-process redactions and edits so that events that get + // redacted/replaced in the same batch landed in the timeline already + // treated. + // NB: We have to store redacting/replacing events to the timeline too - + // see #220. + auto it = std::find_if(events.begin(), events.end(), isEditing); + for (const auto& eptr: RoomEventsRange(it, 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(), it, + [id = r->redactedEvent()]( + const RoomEventPtr& ep) { + return ep->id() == id; + }); + if (targetIt != it) + *targetIt = makeRedacted(**targetIt, *r); + else + qCDebug(MAIN) + << "Redaction" << r->id() << "ignored: target event" + << r->redactedEvent() << "is not found"; + // If the target event comes later, it comes already redacted. + } + if (auto* msg = eventCast<RoomMessageEvent>(eptr)) { + if (!msg->replacedEvent().isEmpty()) { + if (processReplacement(*msg)) + continue; + auto targetIt = std::find_if(events.begin(), it, + [id = msg->replacedEvent()]( + const RoomEventPtr& ep) { + return ep->id() == id; + }); + if (targetIt != it) + *targetIt = makeReplaced(**targetIt, *msg); + else // FIXME: don't ignore, just show it wherever it arrived + qCDebug(MAIN) << "Replacing event" << msg->id() + << "ignored: replaced event" + << msg->replacedEvent() << "is not found"; + // Same as with redactions above, the replaced event coming + // later will come already with the new content. + } + } } + } // State changes arrive as a part of timeline; the current room state gets // updated before merging events to the timeline because that's what @@ -2098,6 +2253,14 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (totalInserted > 0) { + for (auto it = from; it != timeline.cend(); ++it) { + if (const auto* reaction = it->viewAs<ReactionEvent>()) { + const auto& relation = reaction->relation(); + relations[{ relation.eventId, relation.type }] << reaction; + emit q->updatedEvent(relation.eventId); + } + } + qCDebug(MAIN) << "Room" << q->objectName() << "received" << totalInserted << "new events; the last event is now" << timeline.back(); @@ -2156,6 +2319,13 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) q->onAddHistoricalTimelineEvents(from); emit q->addedMessages(timeline.front().index(), from->index()); + for (auto it = from; it != timeline.crend(); ++it) { + if (const auto* reaction = it->viewAs<ReactionEvent>()) { + const auto& relation = reaction->relation(); + relations[{ relation.eventId, relation.type }] << reaction; + emit q->updatedEvent(relation.eventId); + } + } if (from <= q->readMarker()) updateUnreadCount(from, timeline.crend()); @@ -2183,15 +2353,31 @@ Room::Changes Room::processStateEvent(const RoomEvent& 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; + , [] (const RoomAliasesEvent&) { + // This event has been removed by MSC-2432 + return NoChange; } - , [this] (const RoomCanonicalAliasEvent& evt) { + , [this, oldStateEvent] (const RoomCanonicalAliasEvent& evt) { setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); + + auto prevAliases = oldStateEvent ? fromJson<QStringList>( + oldStateEvent->contentJson()["alt_aliases"]) + : QStringList(); + if (oldStateEvent) { + const auto prevCanonicalAlias = + static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent) + ->alias(); + if (!prevCanonicalAlias.isEmpty()) + prevAliases.push_back(prevCanonicalAlias); + } + + auto newAliases = + fromJson<QStringList>(evt.contentJson()["alt_aliases"]); + if (!evt.alias().isEmpty()) + newAliases.push_back(evt.alias()); + + connection()->updateRoomAliases(id(), prevAliases, newAliases); + return CanonicalAliasChange; } , [] (const RoomTopicEvent&) { |