diff options
-rw-r--r-- | room.cpp | 28 | ||||
-rw-r--r-- | user.cpp | 307 | ||||
-rw-r--r-- | user.h | 36 |
3 files changed, 289 insertions, 82 deletions
@@ -142,7 +142,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, QString oldName, const Room* context); void removeMemberFromMap(const QString& username, User* u); void getPreviousContent(int limit = 10); @@ -286,7 +286,7 @@ QImage Room::avatar(int width, int height) auto theOtherOneIt = d->membersMap.begin(); if (theOtherOneIt.value() == localUser()) ++theOtherOneIt; - return (*theOtherOneIt)->avatar(width, height, + return (*theOtherOneIt)->avatar(width, height, this, [=] { emit avatarChanged(); }); } return {}; @@ -300,7 +300,7 @@ User* Room::user(const QString& userId) const JoinState Room::memberJoinState(User* user) const { return - d->membersMap.contains(user->name(), user) ? JoinState::Join : + d->membersMap.contains(user->name(this), user) ? JoinState::Join : JoinState::Leave; } @@ -720,7 +720,7 @@ int Room::timelineSize() const void Room::Private::insertMemberIntoMap(User *u) { - const auto userName = u->name(); + const auto userName = u->name(q); auto namesakes = membersMap.values(userName); membersMap.insert(userName, u); // If there is exactly one namesake of the added user, signal member renaming @@ -729,12 +729,14 @@ 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, QString oldName, const Room* context) { + if (context != q) + return; // It's not a rename for this room if (q->memberJoinState(u) == JoinState::Join) { qCWarning(MAIN) << "Room::Private::renameMember(): the user " - << u->fullName() + << u->fullName(q) << "is already known in the room under a new name."; return; } @@ -797,7 +799,7 @@ QString Room::roomMembername(const User* u) const { // See the CS spec, section 11.2.2.3 - const auto username = u->name(); + const auto username = u->name(this); if (username.isEmpty()) return u->id(); @@ -820,7 +822,7 @@ QString Room::roomMembername(const User* u) const // } // In case of more than one namesake, use the full name to disambiguate - return u->fullName(); + return u->fullName(this); } QString Room::roomMembername(const QString& userId) const @@ -1319,14 +1321,14 @@ void Room::processStateEvents(const RoomEvents& events) case EventType::RoomMember: { auto memberEvent = static_cast<RoomMemberEvent*>(event); auto u = user(memberEvent->userId()); - u->processEvent(memberEvent); + u->processEvent(memberEvent, this); if( memberEvent->membership() == MembershipType::Join ) { if (memberJoinState(u) != JoinState::Join) { d->insertMemberIntoMap(u); connect(u, &User::nameChanged, this, - std::bind(&Private::renameMember, d, u, _2)); + std::bind(&Private::renameMember, d, u, _2, _3)); emit userAdded(u); } } @@ -1336,7 +1338,7 @@ void Room::processStateEvents(const RoomEvents& events) { if (!d->membersLeft.contains(u)) d->membersLeft.append(u); - d->removeMemberFromMap(u->name(), u); + d->removeMemberFromMap(u->name(this), u); emit userRemoved(u); } } @@ -1532,8 +1534,8 @@ QJsonObject Room::Private::toJson() const { QJsonObject content; content.insert("membership", QStringLiteral("join")); - content.insert("displayname", m->name()); - content.insert("avatar_url", m->avatarUrl().toString()); + content.insert("displayname", m->name(q)); + content.insert("avatar_url", m->avatarUrl(q).toString()); QJsonObject memberEvent; memberEvent.insert("type", QStringLiteral("m.room.member")); @@ -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); } @@ -24,8 +24,10 @@ namespace QMatrixClient { - class Event; class Connection; + class Room; + class RoomMemberEvent; + class User: public QObject { Q_OBJECT @@ -51,7 +53,7 @@ namespace QMatrixClient * it. * \sa displayName */ - QString name() const; + QString name(const Room* room = nullptr) const; /** Get the displayed user name * This method returns the result of name() if its non-empty; @@ -60,7 +62,7 @@ namespace QMatrixClient * should be disambiguated. * \sa name, id, fullName Room::roomMembername */ - QString displayname() const; + QString displayname(const Room* room = nullptr) const; /** Get user name and id in one string * The constructed string follows the format 'name (id)' @@ -68,7 +70,7 @@ namespace QMatrixClient * places. * \sa displayName, Room::roomMembername */ - QString fullName() const; + QString fullName(const Room* room = nullptr) const; /** * Returns the name of bridge the user is connected from or empty. @@ -82,32 +84,34 @@ namespace QMatrixClient */ bool isGuest() const; - const Avatar& avatarObject() const; - Q_INVOKABLE QImage avatar(int dimension); - Q_INVOKABLE QImage avatar(int width, int height); - QImage avatar(int width, int height, Avatar::get_callback_t callback); + const Avatar& avatarObject(const Room* room = nullptr) const; + Q_INVOKABLE QImage avatar(int dimension, const Room* room = nullptr); + Q_INVOKABLE QImage avatar(int requestedWidth, int requestedHeight, + const Room* room = nullptr); + QImage avatar(int width, int height, const Room* room, Avatar::get_callback_t callback); - QString avatarMediaId() const; - QUrl avatarUrl() const; + QString avatarMediaId(const Room* room = nullptr) const; + QUrl avatarUrl(const Room* room = nullptr) const; - void processEvent(Event* event); + void processEvent(RoomMemberEvent* event, const Room* r = nullptr); public slots: void rename(const QString& newName); + void rename(const QString& newName, const Room* r); bool setAvatar(const QString& fileName); bool setAvatar(QIODevice* source); signals: - void nameChanged(QString newName, QString oldName); - void avatarChanged(User* user); + void nameChanged(QString newName, QString oldName, + const Room* roomContext); + void avatarChanged(User* user, const Room* roomContext); private slots: - void updateName(const QString& newName); - void updateAvatarUrl(const QUrl& newUrl); + void updateName(const QString& newName, const Room* room = nullptr); private: class Private; - Private* d; + QScopedPointer<Private> d; }; } Q_DECLARE_METATYPE(QMatrixClient::User*) |