aboutsummaryrefslogtreecommitdiff
path: root/lib/room.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/room.cpp')
-rw-r--r--lib/room.cpp225
1 files changed, 156 insertions, 69 deletions
diff --git a/lib/room.cpp b/lib/room.cpp
index 5da9373e..2ce37acc 100644
--- a/lib/room.cpp
+++ b/lib/room.cpp
@@ -105,6 +105,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;
@@ -167,7 +168,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
@@ -184,7 +185,7 @@ 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 =
@@ -235,8 +236,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
@@ -340,7 +341,7 @@ const QString& Room::id() const
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
@@ -369,6 +370,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();
@@ -389,6 +395,11 @@ QString Room::displayName() const
return d->displayname;
}
+void Room::refreshDisplayName()
+{
+ d->updateDisplayname();
+}
+
QString Room::topic() const
{
return d->getCurrentState<RoomTopicEvent>()->topic();
@@ -540,8 +551,8 @@ Room::Changes Room::Private::promoteReadMarker(User* u, rev_iter_t newMarker,
{
const auto oldUnreadCount = unreadMessages;
QElapsedTimer et; et.start();
- unreadMessages = count_if(eagerMarker, timeline.cend(),
- std::bind(&Room::Private::isEventNotable, this, _1));
+ unreadMessages = int(count_if(eagerMarker, timeline.cend(),
+ std::bind(&Room::Private::isEventNotable, this, _1)));
if (et.nsecsElapsed() > profilerMinNsecs() / 10)
qCDebug(PROFILER) << "Recounting unread messages took" << et;
@@ -579,8 +590,8 @@ Room::Changes Room::Private::markMessagesAsRead(rev_iter_t upToMarker)
{
if ((*upToMarker)->senderId() != q->localUser()->id())
{
- connection->callApi<PostReceiptJob>(id, "m.read",
- (*upToMarker)->id());
+ connection->callApi<PostReceiptJob>(id, QStringLiteral("m.read"),
+ QUrl::toPercentEncoding((*upToMarker)->id()));
break;
}
}
@@ -600,9 +611,12 @@ void Room::markAllMessagesAsRead()
bool Room::canSwitchVersions() const
{
+ if (!successorId().isEmpty())
+ return false; // Noone can upgrade a room that's already upgraded
+
// TODO, #276: m.room.power_levels
const auto* plEvt =
- d->currentState.value({"m.room.power_levels", ""});
+ d->currentState.value({QStringLiteral("m.room.power_levels"), {}});
if (!plEvt)
return true;
@@ -612,7 +626,7 @@ bool Room::canSwitchVersions() const
.value(localUser()->id()).toInt(
plJson.value("users_default"_ls).toInt());
const auto tombstonePowerLevel =
- plJson.value("events").toObject()
+ plJson.value("events"_ls).toObject()
.value("m.room.tombstone"_ls).toInt(
plJson.value("state_default"_ls).toInt());
return currentUserLevel >= tombstonePowerLevel;
@@ -815,7 +829,7 @@ void Room::resetNotificationCount()
if( d->notificationCount == 0 )
return;
d->notificationCount = 0;
- emit notificationCountChanged(this);
+ emit notificationCountChanged();
}
int Room::highlightCount() const
@@ -828,15 +842,22 @@ 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
@@ -938,7 +959,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();
}
@@ -1174,7 +1195,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));
@@ -1183,7 +1208,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)
{
@@ -1196,7 +1221,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)
@@ -1212,7 +1236,6 @@ void Room::Private::removeMemberFromMap(const QString& username, User* u)
membersMap.remove(username, u);
// If there was one namesake besides the removed user, signal member renaming
// for it because it doesn't need to be disambiguated anymore.
- // TODO: Think about left users.
if (namesake)
emit q->memberRenamed(namesake);
}
@@ -1222,7 +1245,7 @@ inline auto makeErrorStr(const Event& e, QByteArray msg)
return msg.append("; event dump follows:\n").append(e.originalJson());
}
-Room::Timeline::difference_type Room::Private::moveEventsToTimeline(
+Room::Timeline::size_type Room::Private::moveEventsToTimeline(
RoomEventsRange events, EventsPlacement placement)
{
Q_ASSERT(!events.empty());
@@ -1327,7 +1350,6 @@ 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));
@@ -1343,15 +1365,16 @@ 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);
@@ -1395,7 +1418,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, txnId, call));
@@ -1411,7 +1434,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);
@@ -1430,7 +1453,7 @@ void Room::Private::onEventSendingFailure(const QString& txnId, BaseJob* call)
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)
@@ -1521,12 +1544,17 @@ 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) {
@@ -1634,7 +1662,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);
@@ -1734,8 +1762,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,
@@ -1785,7 +1813,14 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename)
Q_ASSERT(false);
return;
}
- const auto fileUrl = event->content()->fileInfo()->url;
+ const auto* const fileInfo = event->content()->fileInfo();
+ if (!fileInfo->isValid())
+ {
+ qCWarning(MAIN) << "Event" << eventId
+ << "has an empty or malformed mxc URL; won't download";
+ return;
+ }
+ const auto fileUrl = fileInfo->url;
auto filePath = localFilename.toLocalFile();
if (filePath.isEmpty())
{
@@ -1949,10 +1984,10 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction)
{
const StateEventKey evtKey { oldEvent->matrixType(), oldEvent->stateKey() };
Q_ASSERT(currentState.contains(evtKey));
- if (currentState[evtKey] == oldEvent.get())
+ if (currentState.value(evtKey) == oldEvent.get())
{
Q_ASSERT(ti.index() >= 0); // Historical states can't be in currentState
- qCDebug(MAIN).nospace() << "Reverting state "
+ qCDebug(MAIN).nospace() << "Redacting state "
<< oldEvent->matrixType() << "/" << oldEvent->stateKey();
// Retarget the current state to the newly made event.
if (q->processStateEvent(*ti))
@@ -2021,7 +2056,7 @@ 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(),
@@ -2153,7 +2188,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& 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
@@ -2179,16 +2214,52 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
emit avatarChanged();
return AvatarChange;
}
- , [this] (const RoomMemberEvent& evt) {
+ , [this,oldStateEvent] (const RoomMemberEvent& evt) {
auto* u = user(evt.userId());
- u->processEvent(evt, this);
- if (u == localUser() && memberJoinState(u) == JoinState::Invite
+ const auto* oldMemberEvent =
+ static_cast<const RoomMemberEvent*>(oldStateEvent);
+ u->processEvent(evt, this, oldMemberEvent == nullptr);
+ const auto prevMembership = oldMemberEvent
+ ? oldMemberEvent->membership() : MembershipType::Leave;
+ if (u == localUser() && evt.membership() == MembershipType::Invite
&& evt.isDirect())
connection()->addToDirectChats(this, user(evt.senderId()));
- if( evt.membership() == MembershipType::Join )
+ switch (prevMembership)
+ {
+ case MembershipType::Invite:
+ if (evt.membership() != prevMembership)
+ {
+ d->usersInvited.removeOne(u);
+ Q_ASSERT(!d->usersInvited.contains(u));
+ }
+ break;
+ case MembershipType::Join:
+ if (evt.membership() == MembershipType::Invite)
+ qCWarning(MAIN)
+ << "Invalid membership change from Join to Invite:"
+ << evt;
+ if (evt.membership() != prevMembership)
+ {
+ disconnect(u, &User::nameAboutToChange, this, nullptr);
+ disconnect(u, &User::nameChanged, this, nullptr);
+ d->removeMemberFromMap(u->name(this), u);
+ emit userRemoved(u);
+ }
+ break;
+ default:
+ if (evt.membership() == MembershipType::Invite
+ || evt.membership() == MembershipType::Join)
+ {
+ d->membersLeft.removeOne(u);
+ Q_ASSERT(!d->membersLeft.contains(u));
+ }
+ }
+
+ switch(evt.membership())
{
- if (memberJoinState(u) != JoinState::Join)
+ case MembershipType::Join:
+ if (prevMembership != MembershipType::Join)
{
d->insertMemberIntoMap(u);
connect(u, &User::nameAboutToChange, this,
@@ -2199,22 +2270,21 @@ Room::Changes Room::processStateEvent(const RoomEvent& e)
connect(u, &User::nameChanged, this,
[=] (QString, QString oldName, const Room* context) {
if (context == this)
+ {
d->renameMember(u, oldName);
+ emit memberRenamed(u);
+ }
});
emit userAdded(u);
}
- }
- 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);
- }
+ 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;
}
@@ -2395,42 +2465,59 @@ 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);
+ 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);
+ 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);