diff options
Diffstat (limited to 'lib/user.cpp')
-rw-r--r-- | lib/user.cpp | 452 |
1 files changed, 128 insertions, 324 deletions
diff --git a/lib/user.cpp b/lib/user.cpp index bfd23ae2..4c3fc9e2 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -1,365 +1,215 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "user.h" +#include "avatar.h" #include "connection.h" #include "room.h" -#include "avatar.h" + +#include "csapi/content-repo.h" +#include "csapi/profile.h" +#include "csapi/room_state.h" + #include "events/event.h" #include "events/roommemberevent.h" -#include "csapi/room_state.h" -#include "csapi/profile.h" -#include "csapi/content-repo.h" -#include <QtCore/QTimer> -#include <QtCore/QRegularExpression> +#include <QtCore/QElapsedTimer> #include <QtCore/QPointer> +#include <QtCore/QRegularExpression> #include <QtCore/QStringBuilder> -#include <QtCore/QElapsedTimer> +#include <QtCore/QTimer> #include <functional> -using namespace QMatrixClient; -using namespace std::placeholders; +using namespace Quotient; using std::move; -class User::Private -{ - public: - static Avatar makeAvatar(QUrl url) - { - return Avatar(move(url)); - } - - Private(QString userId, Connection* connection) - : userId(move(userId)), connection(connection) - { } - - QString userId; - Connection* connection; - - QString bridged; - QString mostUsedName; - QMultiHash<QString, const Room*> otherNames; - Avatar mostUsedAvatar { makeAvatar({}) }; - std::vector<Avatar> otherAvatars; - auto otherAvatar(QUrl url) - { - return std::find_if(otherAvatars.begin(), otherAvatars.end(), - [&url] (const auto& av) { return av.url() == url; }); - } - QMultiHash<QUrl, const Room*> avatarsToRooms; - - mutable int totalRooms = 0; - - QString nameForRoom(const Room* r, const QString& hint = {}) const; - void setNameForRoom(const Room* r, QString newName, QString oldName); - QUrl avatarUrlForRoom(const Room* r, const QUrl& hint = {}) const; - void setAvatarForRoom(const Room* r, const QUrl& newUrl, - const QUrl& oldUrl); - - void setAvatarOnServer(QString contentUri, User* q); - +class User::Private { +public: + Private(QString userId) : id(move(userId)), hueF(stringToHueF(id)) { } + + QString id; + qreal hueF; + + QString defaultName; + Avatar defaultAvatar; + // NB: This container is ever-growing. Even if the user no more scrolls + // the timeline that far back, historical avatars are still kept around. + // This is consistent with the rest of Quotient, as room timelines + // are never vacuumed either. This will probably change in the future. + /// Map of mediaId to Avatar objects + static UnorderedMap<QString, Avatar> otherAvatars; }; - -QString User::Private::nameForRoom(const Room* r, const QString& hint) const -{ - // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedName || otherNames.contains(hint, r)) - return hint; - return otherNames.key(r, mostUsedName); -} - -static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; - -void User::Private::setNameForRoom(const Room* r, QString newName, - QString oldName) -{ - Q_ASSERT(oldName != newName); - Q_ASSERT(oldName == mostUsedName || otherNames.contains(oldName, r)); - if (totalRooms < 2) - { - Q_ASSERT_X(totalRooms > 0 && otherNames.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedName = move(newName); - return; - } - otherNames.remove(oldName, r); - 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 << "to switch the most used name"; - } - else - otherNames.insert(newName, r); - } -} - -QUrl User::Private::avatarUrlForRoom(const Room* r, const QUrl& hint) const -{ - // If the hint is accurate, this function is O(1) instead of O(n) - if (hint == mostUsedAvatar.url() || avatarsToRooms.contains(hint, r)) - return hint; - auto it = std::find(avatarsToRooms.begin(), avatarsToRooms.end(), r); - return it == avatarsToRooms.end() ? mostUsedAvatar.url() : it.key(); -} - -void User::Private::setAvatarForRoom(const Room* r, const QUrl& newUrl, - const QUrl& oldUrl) -{ - Q_ASSERT(oldUrl != newUrl); - Q_ASSERT(oldUrl == mostUsedAvatar.url() || - avatarsToRooms.contains(oldUrl, r)); - if (totalRooms < 2) - { - Q_ASSERT_X(totalRooms > 0 && otherAvatars.empty(), __FUNCTION__, - "Internal structures inconsistency"); - mostUsedAvatar.updateUrl(newUrl); - return; - } - avatarsToRooms.remove(oldUrl, r); - if (!avatarsToRooms.contains(oldUrl)) - { - auto it = otherAvatar(oldUrl); - if (it != otherAvatars.end()) - otherAvatars.erase(it); - } - if (newUrl != mostUsedAvatar.url()) - { - // Check if the new avatar is about to become most used. - if (avatarsToRooms.count(newUrl) >= totalRooms - avatarsToRooms.size()) - { - QElapsedTimer et; - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - { - qCDebug(MAIN) << "Switching the most used avatar of user" << userId - << "from" << mostUsedAvatar.url().toDisplayString() - << "to" << newUrl.toDisplayString(); - et.start(); - } - avatarsToRooms.remove(newUrl); - auto nextMostUsedIt = otherAvatar(newUrl); - Q_ASSERT(nextMostUsedIt != otherAvatars.end()); - std::swap(mostUsedAvatar, *nextMostUsedIt); - for (const auto* r1: connection->roomMap()) - if (avatarUrlForRoom(r1) == nextMostUsedIt->url()) - avatarsToRooms.insert(nextMostUsedIt->url(), r1); - - if (totalRooms > MIN_JOINED_ROOMS_TO_LOG) - qCDebug(PROFILER) << et << "to switch the most used avatar"; - } else { - if (otherAvatar(newUrl) == otherAvatars.end()) - otherAvatars.emplace_back(makeAvatar(newUrl)); - avatarsToRooms.insert(newUrl, r); - } - } -} +decltype(User::Private::otherAvatars) User::Private::otherAvatars {}; User::User(QString userId, Connection* connection) - : QObject(connection), d(new Private(move(userId), connection)) + : QObject(connection), d(makeImpl<Private>(move(userId))) { - setObjectName(userId); + setObjectName(id()); + if (connection->userId() == id()) { + // Load profile information for local user. + load(); + } } Connection* User::connection() const { - Q_ASSERT(d->connection); - return d->connection; + Q_ASSERT(parent()); + return static_cast<Connection*>(parent()); } -User::~User() = default; - -QString User::id() const +void User::load() { - return d->userId; + auto* profileJob = + connection()->callApi<GetUserProfileJob>(id()); + connect(profileJob, &BaseJob::result, this, [this, profileJob] { + d->defaultName = profileJob->displayname(); + d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl())); + emit defaultNameChanged(); + emit defaultAvatarChanged(); + }); } +QString User::id() const { return d->id; } + bool User::isGuest() const { - Q_ASSERT(!d->userId.isEmpty() && d->userId.startsWith('@')); - auto it = std::find_if_not(d->userId.begin() + 1, d->userId.end(), - [] (QChar c) { return c.isDigit(); }); - Q_ASSERT(it != d->userId.end()); + Q_ASSERT(!d->id.isEmpty() && d->id.startsWith('@')); + auto it = std::find_if_not(d->id.cbegin() + 1, d->id.cend(), + [](QChar c) { return c.isDigit(); }); + Q_ASSERT(it != d->id.cend()); return *it == ':'; } -QString User::name(const Room* room) const -{ - return d->nameForRoom(room); -} - -QString User::rawName(const Room* room) const -{ - return d->bridged.isEmpty() ? name(room) : - name(room) % " (" % d->bridged % ')'; -} +int User::hue() const { return int(hueF() * 359); } -void User::updateName(const QString& newName, const Room* room) -{ - updateName(newName, d->nameForRoom(room), room); -} - -void User::updateName(const QString& newName, const QString& oldName, - const Room* room) +QString User::name(const Room* room) const { - Q_ASSERT(oldName == d->mostUsedName || d->otherNames.contains(oldName, room)); - if (newName != oldName) - { - emit nameAboutToChange(newName, oldName, room); - d->setNameForRoom(room, newName, oldName); - setObjectName(displayname()); - emit nameChanged(newName, oldName, room); - } -} - -void User::updateAvatarUrl(const QUrl& newUrl, const QUrl& oldUrl, - const Room* room) -{ - Q_ASSERT(oldUrl == d->mostUsedAvatar.url() || - d->avatarsToRooms.contains(oldUrl, room)); - if (newUrl != oldUrl) - { - d->setAvatarForRoom(room, newUrl, oldUrl); - setObjectName(displayname()); - emit avatarChanged(this, room); - } - + return room ? room->memberName(id()) : d->defaultName; } void User::rename(const QString& newName) { - auto job = connection()->callApi<SetDisplayNameJob>(id(), newName); - connect(job, &BaseJob::success, this, [=] { updateName(newName); }); + const auto actualNewName = sanitized(newName); + if (actualNewName == d->defaultName) + return; // Nothing to do + + connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName), + &BaseJob::success, this, [this, actualNewName] { + // Check again, it could have changed meanwhile + if (actualNewName != d->defaultName) { + d->defaultName = actualNewName; + emit defaultNameChanged(); + } else + qCWarning(MAIN) + << "User" << id() << "already has profile name set to" + << actualNewName; + }); } -void User::rename(const QString& newName, const Room* r) +void User::rename(const QString& newName, Room* r) { - if (!r) - { + if (!r) { qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" "is incorrect; client developer, please fix it"; rename(newName); return; } - 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 = r->setMemberState(id(), RoomMemberEvent(move(evtC))); - connect(job, &BaseJob::success, this, [=] { updateName(newName, r); }); + // #481: take the current state and update it with the new name + if (const auto& maybeEvt = r->currentState().get<RoomMemberEvent>(id())) { + auto content = maybeEvt->content(); + if (content.membership == Membership::Join) { + content.displayName = sanitized(newName); + r->setState<RoomMemberEvent>(id(), move(content)); + // The state will be updated locally after it arrives with sync + return; + } + } + qCCritical(MEMBERS) + << "Attempt to rename a non-member in a room context - ignored"; } -bool User::setAvatar(const QString& fileName) +template <typename SourceT> +inline bool User::doSetAvatar(SourceT&& source) { - return avatarObject().upload(connection(), fileName, - std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); + return d->defaultAvatar.upload( + connection(), source, [this](const QUrl& contentUri) { + auto* j = connection()->callApi<SetAvatarUrlJob>(id(), contentUri); + connect(j, &BaseJob::success, this, + [this, contentUri] { + if (contentUri == d->defaultAvatar.url()) { + d->defaultAvatar.updateUrl(contentUri); + emit defaultAvatarChanged(); + } else + qCWarning(MAIN) << "User" << id() + << "already has avatar URL set to" + << contentUri.toDisplayString(); + }); + }); } -bool User::setAvatar(QIODevice* source) +bool User::setAvatar(const QString& fileName) { - return avatarObject().upload(connection(), source, - std::bind(&Private::setAvatarOnServer, d.data(), _1, this)); + return doSetAvatar(fileName); } -void User::requestDirectChat() +bool User::setAvatar(QIODevice* source) { - connection()->requestDirectChat(this); + return doSetAvatar(source); } -void User::ignore() +void User::removeAvatar() { - connection()->addToIgnoredUsers(this); + connection()->callApi<SetAvatarUrlJob>(id(), QUrl()); } -void User::unmarkIgnore() -{ - connection()->removeFromIgnoredUsers(this); -} +void User::requestDirectChat() { connection()->requestDirectChat(this); } -void User::Private::setAvatarOnServer(QString contentUri, User* q) -{ - auto* j = connection->callApi<SetAvatarUrlJob>(userId, contentUri); - connect(j, &BaseJob::success, q, - [=] { q->updateAvatarUrl(contentUri, avatarUrlForRoom(nullptr)); }); -} +void User::ignore() { connection()->addToIgnoredUsers(this); } -QString User::displayname(const Room* room) const -{ - if (room) - return room->roomMembername(this); +void User::unmarkIgnore() { connection()->removeFromIgnoredUsers(this); } - const auto name = d->nameForRoom(nullptr); - return name.isEmpty() ? d->userId : name; -} +bool User::isIgnored() const { return connection()->isIgnored(this); } -QString User::fullName(const Room* room) const +QString User::displayname(const Room* room) const { - const auto name = d->nameForRoom(room); - return name.isEmpty() ? d->userId : name % " (" % d->userId % ')'; + return room ? room->safeMemberName(id()) + : d->defaultName.isEmpty() ? d->id : d->defaultName; } -QString User::bridged() const +QString User::fullName(const Room* room) const { - return d->bridged; + const auto displayName = name(room); + return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')'); } const Avatar& User::avatarObject(const Room* room) const { - auto it = d->otherAvatar(d->avatarUrlForRoom(room)); - return it != d->otherAvatars.end() ? *it : d->mostUsedAvatar; + if (!room) + return d->defaultAvatar; + + const auto& url = room->memberAvatarUrl(id()); + const auto& mediaId = url.authority() + url.path(); + return d->otherAvatars.try_emplace(mediaId, url).first->second; } -QImage User::avatar(int dimension, const Room* room) +QImage User::avatar(int dimension, const Room* room) const { return avatar(dimension, dimension, room); } -QImage User::avatar(int width, int height, const Room* room) +QImage User::avatar(int width, int height, const Room* room) const { - return avatar(width, height, room, []{}); + return avatar(width, height, room, [] {}); } QImage User::avatar(int width, int height, const Room* room, - const Avatar::get_callback_t& callback) + const Avatar::get_callback_t& callback) const { - return avatarObject(room).get(d->connection, width, height, - [=] { emit avatarChanged(this, room); callback(); }); + return avatarObject(room).get(connection(), width, height, callback); } QString User::avatarMediaId(const Room* room) const @@ -372,50 +222,4 @@ QUrl User::avatarUrl(const Room* room) const return avatarObject(room).url(); } -void User::processEvent(const RoomMemberEvent& event, const Room* room) -{ - Q_ASSERT(room); - 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()) - { - 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)); - } - if (event.prevContent()) - { - // FIXME: the hint doesn't work for bridged users - auto oldNameHint = - d->nameForRoom(room, event.prevContent()->displayName); - updateName(newName, oldNameHint, room); - updateAvatarUrl(event.avatarUrl(), - d->avatarUrlForRoom(room, event.prevContent()->avatarUrl), - room); - } else { - updateName(newName, room); - updateAvatarUrl(event.avatarUrl(), d->avatarUrlForRoom(room), room); - } -} +qreal User::hueF() const { return d->hueF; } |