aboutsummaryrefslogtreecommitdiff
path: root/lib/room.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/room.cpp')
-rw-r--r--lib/room.cpp298
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&) {