aboutsummaryrefslogtreecommitdiff
path: root/user.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'user.cpp')
-rw-r--r--user.cpp307
1 files changed, 254 insertions, 53 deletions
diff --git a/user.cpp b/user.cpp
index 289f0bac..2a3071af 100644
--- a/user.cpp
+++ b/user.cpp
@@ -19,9 +19,11 @@
#include "user.h"
#include "connection.h"
+#include "room.h"
#include "avatar.h"
#include "events/event.h"
#include "events/roommemberevent.h"
+#include "jobs/setroomstatejob.h"
#include "jobs/generated/profile.h"
#include "jobs/generated/content-repo.h"
@@ -29,37 +31,208 @@
#include <QtCore/QRegularExpression>
#include <QtCore/QPointer>
#include <QtCore/QStringBuilder>
+#include <QtCore/QElapsedTimer>
#include <functional>
+#include <unordered_set>
using namespace QMatrixClient;
+using std::move;
+using std::exchange;
class User::Private
{
public:
+ static QIcon defaultIcon();
+ static Avatar makeAvatar(QUrl url) { return { url, defaultIcon() }; }
+
Private(QString userId, Connection* connection)
- : userId(std::move(userId)), connection(connection)
+ : userId(move(userId)), connection(connection)
{ }
QString userId;
- QString name;
- QString bridged;
Connection* connection;
- Avatar avatar { QIcon::fromTheme(QStringLiteral("user-available")) };
+
+ QString mostUsedName;
+ QString bridged;
+ Avatar mostUsedAvatar { defaultIcon() };
+ QMultiHash<QString, const Room*> otherNames;
+ std::vector<std::pair<Avatar,
+ std::unordered_set<const Room*>>> otherAvatars;
+
+ mutable int totalRooms = 0;
+
+ QString nameForRoom(const Room* r) const;
+ std::pair<bool, QString> setNameForRoom(const Room* r, QString newName);
+ const Avatar& avatarForRoom(const Room* r) const;
+ bool setAvatarUrlForRoom(const Room* r, const QUrl& avatarUrl);
void setAvatar(QString contentUri, User* q);
+
};
+QIcon User::Private::defaultIcon()
+{
+ static const QIcon icon
+ { QIcon::fromTheme(QStringLiteral("user-available")) };
+ return icon;
+}
+
+QString User::Private::nameForRoom(const Room* r) const
+{
+ return otherNames.key(r, mostUsedName);
+}
+
+static constexpr int MIN_JOINED_ROOMS_TO_LOG = 100;
+
+std::pair<bool, QString> User::Private::setNameForRoom(const Room* r,
+ QString newName)
+{
+ if (totalRooms < 2)
+ {
+ Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__,
+ "Internal structures inconsistency");
+ // The below uses that initialization list evaluation is ordered
+ return { mostUsedName != newName,
+ exchange(mostUsedName, move(newName)) };
+ }
+ QString oldName;
+ // The below works because QMultiHash iterators dereference to stored values
+ auto it = std::find(otherNames.begin(), otherNames.end(), r);
+ if (it != otherNames.end())
+ {
+ oldName = it.key();
+ if (oldName == newName)
+ return { false, oldName }; // old name and new name coincide
+ otherNames.erase(it);
+ }
+ if (newName != mostUsedName)
+ {
+ // Check if the newName is about to become most used.
+ if (otherNames.count(newName) >= totalRooms - otherNames.size())
+ {
+ Q_ASSERT(totalRooms > 1);
+ QElapsedTimer et;
+ if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
+ {
+ qCDebug(MAIN) << "Switching the most used name of user" << userId
+ << "from" << mostUsedName << "to" << newName;
+ qCDebug(MAIN) << "The user is in" << totalRooms << "rooms";
+ et.start();
+ }
+
+ for (auto* r1: connection->roomMap())
+ if (nameForRoom(r1) == mostUsedName)
+ otherNames.insert(mostUsedName, r1);
+
+ mostUsedName = newName;
+ otherNames.remove(newName);
+ if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
+ qCDebug(PROFILER) << et.elapsed()
+ << "ms to switch the most used name";
+ }
+ else
+ otherNames.insert(newName, r);
+ }
+ return { true, oldName };
+}
+
+const Avatar& User::Private::avatarForRoom(const Room* r) const
+{
+ for (const auto& p: otherAvatars)
+ {
+ auto roomIt = p.second.find(r);
+ if (roomIt != p.second.end())
+ return p.first;
+ }
+ return mostUsedAvatar;
+}
+
+bool User::Private::setAvatarUrlForRoom(const Room* r, const QUrl& avatarUrl)
+{
+ if (totalRooms < 2)
+ {
+ Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__,
+ "Internal structures inconsistency");
+ return
+ exchange(mostUsedAvatar, makeAvatar(avatarUrl)).url() != avatarUrl;
+ }
+ for (auto it = otherAvatars.begin(); it != otherAvatars.end(); ++it)
+ {
+ auto roomIt = it->second.find(r);
+ if (roomIt != it->second.end())
+ {
+ if (it->first.url() == avatarUrl)
+ return false; // old url and new url coincide
+ it->second.erase(r);
+ if (avatarUrl == mostUsedAvatar.url())
+ {
+ if (it->second.empty())
+ otherAvatars.erase(it);
+ // The most used avatar will be used for this room
+ return true;
+ }
+ }
+ }
+ if (avatarUrl != mostUsedAvatar.url())
+ {
+ size_t othersCount = 0;
+ auto entryToReplace = otherAvatars.end();
+ for (auto it = otherAvatars.begin(); it != otherAvatars.end(); ++it)
+ {
+ othersCount += it->second.size();
+ if (it->first.url() == avatarUrl)
+ entryToReplace = it;
+ }
+ // Check if the new avatar is about to become most used.
+ if (entryToReplace != otherAvatars.end() &&
+ entryToReplace->second.size() >= size_t(totalRooms) - othersCount)
+ {
+ QElapsedTimer et;
+ if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
+ {
+ qCDebug(MAIN) << "Switching the most used avatar of user" << userId
+ << "from" << mostUsedAvatar.url().toDisplayString()
+ << "to" << avatarUrl.toDisplayString();
+ et.start();
+ }
+ entryToReplace->first =
+ exchange(mostUsedAvatar, makeAvatar(avatarUrl));
+ entryToReplace->second.clear();
+ for (const auto* r1: connection->roomMap())
+ {
+ if (avatarForRoom(r1).url() == mostUsedAvatar.url())
+ entryToReplace->second.insert(r1);
+ }
+ if (totalRooms > MIN_JOINED_ROOMS_TO_LOG)
+ qCDebug(PROFILER) << et.elapsed()
+ << "ms to switch the most used avatar";
+ }
+ }
+ if (avatarUrl != mostUsedAvatar.url()) // It could have changed above
+ {
+ // Create a new entry if necessary and add the room to it.
+ auto it = find_if(otherAvatars.begin(), otherAvatars.end(),
+ [&avatarUrl] (const auto& p) {
+ return p.first.url() == avatarUrl;
+ });
+ if (it == otherAvatars.end())
+ {
+ otherAvatars.push_back({ Avatar(avatarUrl, defaultIcon()), {} });
+ it = otherAvatars.end() - 1;
+ }
+ it->second.insert(r);
+ }
+ return true;
+}
+
User::User(QString userId, Connection* connection)
: QObject(connection), d(new Private(std::move(userId), connection))
{
setObjectName(userId);
}
-User::~User()
-{
- delete d;
-}
+User::~User() = default;
QString User::id() const
{
@@ -75,34 +248,44 @@ bool User::isGuest() const
return *it == ':';
}
-QString User::name() const
+QString User::name(const Room* room) const
{
- return d->name;
+ return d->nameForRoom(room);
}
-void User::updateName(const QString& newName)
+void User::updateName(const QString& newName, const Room* room)
{
- const auto oldName = name();
- if (oldName != newName)
+ const auto setNameResult = d->setNameForRoom(room, newName);
+ if (setNameResult.first)
{
- d->name = newName;
setObjectName(displayname());
- emit nameChanged(newName, oldName);
+ emit nameChanged(newName, setNameResult.second, room);
}
}
-void User::updateAvatarUrl(const QUrl& newUrl)
-{
- if (d->avatar.updateUrl(newUrl))
- emit avatarChanged(this);
-}
-
void User::rename(const QString& newName)
{
auto job = d->connection->callApi<SetDisplayNameJob>(id(), newName);
connect(job, &BaseJob::success, this, [=] { updateName(newName); });
}
+void User::rename(const QString& newName, const Room* r)
+{
+ if (!r)
+ {
+ qCWarning(MAIN) << "Passing a null room to two-argument User::rename()"
+ "is incorrect; client developer, please fix it";
+ rename(newName);
+ }
+ Q_ASSERT_X(r->memberJoinState(this) == JoinState::Join, __FUNCTION__,
+ "Attempt to rename a user that's not a room member");
+ MemberEventContent evtC;
+ evtC.displayName = newName;
+ auto job = d->connection->callApi<SetRoomStateJob>(
+ r->id(), id(), RoomMemberEvent(move(evtC)));
+ connect(job, &BaseJob::success, this, [=] { updateName(newName, r); });
+}
+
bool User::setAvatar(const QString& fileName)
{
return avatarObject().upload(d->connection, fileName,
@@ -118,18 +301,20 @@ bool User::setAvatar(QIODevice* source)
void User::Private::setAvatar(QString contentUri, User* q)
{
auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri);
- connect(j, &BaseJob::success, q, [q] { emit q->avatarChanged(q); });
+ connect(j, &BaseJob::success, q, [q] { emit q->avatarChanged(q, nullptr); });
}
-QString User::displayname() const
+QString User::displayname(const Room* room) const
{
- return d->name.isEmpty() ? d->userId : d->name;
+ auto name = d->nameForRoom(room);
+ return name.isEmpty() ? d->userId :
+ room ? room->roomMembername(name) : name;
}
-QString User::fullName() const
+QString User::fullName(const Room* room) const
{
- return d->name.isEmpty() ? d->userId :
- d->name % " (" % d->userId % ')';
+ auto name = d->nameForRoom(room);
+ return name.isEmpty() ? d->userId : name % " (" % d->userId % ')';
}
QString User::bridged() const
@@ -137,54 +322,70 @@ QString User::bridged() const
return d->bridged;
}
-const Avatar& User::avatarObject() const
+const Avatar& User::avatarObject(const Room* room) const
{
- return d->avatar;
+ return d->avatarForRoom(room);
}
-QImage User::avatar(int dimension)
+QImage User::avatar(int dimension, const Room* room)
{
- return avatar(dimension, dimension);
+ return avatar(dimension, dimension, room);
}
-QImage User::avatar(int width, int height)
+QImage User::avatar(int width, int height, const Room* room)
{
- return avatar(width, height, []{});
+ return avatar(width, height, room, []{});
}
-QImage User::avatar(int width, int height, Avatar::get_callback_t callback)
+QImage User::avatar(int width, int height, const Room* room, Avatar::get_callback_t callback)
{
- return avatarObject().get(d->connection, width, height,
- [this,callback] { emit avatarChanged(this); callback(); });
+ return avatarObject(room).get(d->connection, width, height,
+ [=] { emit avatarChanged(this, room); callback(); });
}
-QString User::avatarMediaId() const
+QString User::avatarMediaId(const Room* room) const
{
- return avatarObject().mediaId();
+ return avatarObject(room).mediaId();
}
-QUrl User::avatarUrl() const
+QUrl User::avatarUrl(const Room* room) const
{
- return avatarObject().url();
+ return avatarObject(room).url();
}
-void User::processEvent(Event* event)
+void User::processEvent(RoomMemberEvent* event, const Room* room)
{
- if( event->type() == EventType::RoomMember )
+ if (event->membership() != MembershipType::Invite &&
+ event->membership() != MembershipType::Join)
+ return;
+
+ auto aboutToEnter = room->memberJoinState(this) == JoinState::Leave &&
+ (event->membership() == MembershipType::Join ||
+ event->membership() == MembershipType::Invite);
+ if (aboutToEnter)
+ ++d->totalRooms;
+
+ auto newName = event->displayName();
+ // `bridged` value uses the same notification signal as the name;
+ // it is assumed that first setting of the bridge occurs together with
+ // the first setting of the name, and further bridge updates are
+ // exceptionally rare (the only reasonable case being that the bridge
+ // changes the naming convention). For the same reason room-specific
+ // bridge tags are not supported at all.
+ QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$");
+ auto match = reSuffix.match(newName);
+ if (match.hasMatch())
{
- auto e = static_cast<RoomMemberEvent*>(event);
- if (e->membership() == MembershipType::Leave)
- return;
-
- auto newName = e->displayName();
- QRegularExpression reSuffix(" \\((IRC|Gitter|Telegram)\\)$");
- auto match = reSuffix.match(newName);
- if (match.hasMatch())
+ if (d->bridged != match.captured(1))
{
+ if (!d->bridged.isEmpty())
+ qCWarning(MAIN) << "Bridge for user" << id() << "changed:"
+ << d->bridged << "->" << match.captured(1);
d->bridged = match.captured(1);
- newName.truncate(match.capturedStart(0));
}
- updateName(newName);
- updateAvatarUrl(e->avatarUrl());
+ newName.truncate(match.capturedStart(0));
}
+ updateName(event->displayName(), room);
+ if (d->setAvatarUrlForRoom(room, event->avatarUrl()))
+ emit avatarChanged(this, room);
}