From 9474a9afeb7ff63538ee85b4b59e172e5d32db32 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Feb 2018 11:16:10 +0900 Subject: Avatar: provide common logic for uploading; don't store Connection Although the setting part of the work is class(User or Room)-specific, the uploading part is common, so Avatar provides it now. Also: there's no need to store Connection, as it's only used in get() and upload() - just pass it as the parameter to the methods. --- room.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'room.cpp') diff --git a/room.cpp b/room.cpp index 73591c98..51c5f5cc 100644 --- a/room.cpp +++ b/room.cpp @@ -64,7 +64,7 @@ class Room::Private Private(Connection* c, QString id_, JoinState initialJoinState) : q(nullptr), connection(c), id(std::move(id_)) - , avatar(c), joinState(initialJoinState) + , joinState(initialJoinState) { } Room* q; @@ -287,7 +287,7 @@ QImage Room::avatar(int dimension) QImage Room::avatar(int width, int height) { if (!d->avatar.url().isEmpty()) - return d->avatar.get(width, height, [=] { emit avatarChanged(); }); + return d->avatar.get(connection(), width, height, [=] { emit avatarChanged(); }); // Use the other side's avatar for 1:1's if (d->membersMap.size() == 2) @@ -295,8 +295,8 @@ QImage Room::avatar(int width, int height) auto theOtherOneIt = d->membersMap.begin(); if (theOtherOneIt.value() == localUser()) ++theOtherOneIt; - return (*theOtherOneIt)->avatarObject() - .get(width, height, [=] { emit avatarChanged(); }); + return (*theOtherOneIt)->avatar(width, height, + [=] { emit avatarChanged(); }); } return {}; } -- cgit v1.2.3 From b243fc6495e06fa4d41562aa20028dfdb3efd28e Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Feb 2018 11:16:10 +0900 Subject: Room: user() and memberJoinState(); code cleanup Room::user() came instead of Room::Private::member() and memberJoinState() came instead of Private::hasMember. --- room.cpp | 169 +++++++++++++++++++++++++++++---------------------------------- room.h | 22 +++++++++ 2 files changed, 100 insertions(+), 91 deletions(-) (limited to 'room.cpp') diff --git a/room.cpp b/room.cpp index 51c5f5cc..3c6fb223 100644 --- a/room.cpp +++ b/room.cpp @@ -140,16 +140,10 @@ class Room::Private const RoomMessageEvent* getEventWithFile(const QString& eventId) const; - // Convenience methods to work with the membersMap and usersLeft. - // addMember() and removeMember() emit respective Room:: signals - // after a succesful operation. //void inviteUser(User* u); // We might get it at some point in time. - void addMember(User* u); - bool hasMember(User* u) const; - // You can't identify a single user by displayname, only by id - User* member(const QString& id) const; + void insertMemberIntoMap(User* u); void renameMember(User* u, QString oldName); - void removeMember(User* u); + void removeMemberFromMap(const QString& username, User* u); void getPreviousContent(int limit = 10); @@ -202,12 +196,9 @@ class Room::Private QString calculateDisplayname() const; QString roomNameFromMemberNames(const QList& userlist) const; - void insertMemberIntoMap(User* u); - void removeMemberFromMap(const QString& username, User* u); - bool isLocalUser(const User* u) const { - return u == connection->user(); + return u == q->localUser(); } }; @@ -301,6 +292,18 @@ QImage Room::avatar(int width, int height) return {}; } +User* Room::user(const QString& userId) const +{ + return connection()->user(userId); +} + +JoinState Room::memberJoinState(User* user) const +{ + return + d->membersMap.contains(user->name(), user) ? JoinState::Join : + JoinState::Leave; +} + JoinState Room::joinState() const { return d->joinState; @@ -700,7 +703,7 @@ QStringList Room::memberNames() const { QStringList res; for (auto u : d->membersMap) - res.append( this->roomMembername(u) ); + res.append( roomMembername(u) ); return res; } @@ -717,12 +720,31 @@ int Room::timelineSize() const void Room::Private::insertMemberIntoMap(User *u) { - auto namesakes = membersMap.values(u->name()); - membersMap.insert(u->name(), u); + const auto userName = u->name(); + auto namesakes = membersMap.values(userName); + membersMap.insert(userName, u); // If there is exactly one namesake of the added user, signal member renaming // for that other one because the two should be disambiguated now. if (namesakes.size() == 1) - emit q->memberRenamed(namesakes[0]); + emit q->memberRenamed(namesakes.front()); +} + +void Room::Private::renameMember(User* u, QString oldName) +{ + if (q->memberJoinState(u) == JoinState::Join) + { + qCWarning(MAIN) << "Room::Private::renameMember(): the user " + << u->fullName() + << "is already known in the room under a new name."; + return; + } + + if (membersMap.contains(oldName, u)) + { + removeMemberFromMap(oldName, u); + insertMemberIntoMap(u); + emit q->memberRenamed(u); + } } void Room::Private::removeMemberFromMap(const QString& username, User* u) @@ -731,9 +753,8 @@ void Room::Private::removeMemberFromMap(const QString& username, User* 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. - auto formerNamesakes = membersMap.values(username); - if (formerNamesakes.size() == 1) - emit q->memberRenamed(formerNamesakes[0]); + if (membersMap.count(username) == 1) + emit q->memberRenamed(membersMap.value(username)); } inline auto makeErrorStr(const Event& e, QByteArray msg) @@ -772,69 +793,17 @@ Room::Timeline::size_type Room::Private::insertEvents(RoomEventsRange&& events, return events.size(); } -void Room::Private::addMember(User *u) -{ - if (!hasMember(u)) - { - insertMemberIntoMap(u); - connect(u, &User::nameChanged, q, - std::bind(&Private::renameMember, this, u, _2)); - emit q->userAdded(u); - } -} - -bool Room::Private::hasMember(User* u) const -{ - return membersMap.values(u->name()).contains(u); -} - -User* Room::Private::member(const QString& id) const -{ - auto u = connection->user(id); - return hasMember(u) ? u : nullptr; -} - -void Room::Private::renameMember(User* u, QString oldName) -{ - if (hasMember(u)) - { - qCWarning(MAIN) << "Room::Private::renameMember(): the user " - << u->name() - << "is already known in the room under a new name."; - return; - } - - if (membersMap.values(oldName).contains(u)) - { - removeMemberFromMap(oldName, u); - insertMemberIntoMap(u); - emit q->memberRenamed(u); - } -} - -void Room::Private::removeMember(User* u) -{ - if (hasMember(u)) - { - if ( !membersLeft.contains(u) ) - membersLeft.append(u); - removeMemberFromMap(u->name(), u); - emit q->userRemoved(u); - } -} - QString Room::roomMembername(const User* u) const { // See the CS spec, section 11.2.2.3 - QString username = u->name(); + const auto username = u->name(); if (username.isEmpty()) return u->id(); // Get the list of users with the same display name. Most likely, // there'll be one, but there's a chance there are more. - auto namesakes = d->membersMap.values(username); - if (namesakes.size() == 1) + if (d->membersMap.count(username) == 1) return username; // We expect a user to be a member of the room - but technically it is @@ -856,7 +825,7 @@ QString Room::roomMembername(const User* u) const QString Room::roomMembername(const QString& userId) const { - return roomMembername(connection()->user(userId)); + return roomMembername(user(userId)); } void Room::updateData(SyncRoomData&& data) @@ -1250,7 +1219,7 @@ void Room::Private::checkUnreadMessages(timeline_iter_t from) // read receipts from the server (or, for the local user, // markMessagesAsRead() invocation) to promote their read markers over // the new message events. - auto firstWriter = connection->user((*from)->senderId()); + auto firstWriter = q->user((*from)->senderId()); if (q->readMarker(firstWriter) != timeline.crend()) { promoteReadMarker(firstWriter, q->findInTimeline((*from)->id())); @@ -1349,16 +1318,27 @@ void Room::processStateEvents(const RoomEvents& events) } case EventType::RoomMember: { auto memberEvent = static_cast(event); - // Can't use d->member() below because the user may be not a member (yet) - auto u = d->connection->user(memberEvent->userId()); - u->processEvent(event); + auto u = user(memberEvent->userId()); + u->processEvent(memberEvent); if( memberEvent->membership() == MembershipType::Join ) { - d->addMember(u); + if (memberJoinState(u) != JoinState::Join) + { + d->insertMemberIntoMap(u); + connect(u, &User::nameChanged, this, + std::bind(&Private::renameMember, d, u, _2)); + emit userAdded(u); + } } else if( memberEvent->membership() == MembershipType::Leave ) { - d->removeMember(u); + if (memberJoinState(u) == JoinState::Join) + { + if (!d->membersLeft.contains(u)) + d->membersLeft.append(u); + d->removeMemberFromMap(u->name(), u); + emit userRemoved(u); + } } break; } @@ -1380,8 +1360,9 @@ void Room::processEphemeralEvent(EventPtr event) d->usersTyping.clear(); for( const QString& userId: typingEvent->users() ) { - if (auto m = d->member(userId)) - d->usersTyping.append(m); + auto u = user(userId); + if (memberJoinState(u) == JoinState::Join) + d->usersTyping.append(u); } emit typingChanged(); break; @@ -1403,8 +1384,11 @@ void Room::processEphemeralEvent(EventPtr event) { const auto newMarker = findInTimeline(p.evtId); for( const Receipt& r: p.receipts ) - if (auto m = d->member(r.userId)) - d->promoteReadMarker(m, newMarker); + { + auto u = user(r.userId); + if (memberJoinState(u) == JoinState::Join) + d->promoteReadMarker(u, newMarker); + } } else { qCDebug(EPHEMERAL) << "Event" << p.evtId @@ -1414,9 +1398,12 @@ void Room::processEphemeralEvent(EventPtr event) // a previous marker for a user, keep the previous marker. // Otherwise, blindly store the event id for this user. for( const Receipt& r: p.receipts ) - if (auto m = d->member(r.userId)) - if (readMarker(m) == timelineEdge()) - d->setLastReadEvent(m, p.evtId); + { + auto u = user(r.userId); + if (memberJoinState(u) == JoinState::Join && + readMarker(u) == timelineEdge()) + d->setLastReadEvent(u, p.evtId); + } } } if (receiptEvent->unreadMessages()) @@ -1541,16 +1528,16 @@ QJsonObject Room::Private::toJson() const appendStateEvent(stateEvents, "m.room.canonical_alias", "alias", canonicalAlias); - for (const auto &i : membersMap) + for (const auto *m : membersMap) { QJsonObject content; content.insert("membership", QStringLiteral("join")); - content.insert("displayname", i->name()); - content.insert("avatar_url", i->avatarUrl().toString()); + content.insert("displayname", m->name()); + content.insert("avatar_url", m->avatarUrl().toString()); QJsonObject memberEvent; memberEvent.insert("type", QStringLiteral("m.room.member")); - memberEvent.insert("state_key", i->id()); + memberEvent.insert("state_key", m->id()); memberEvent.insert("content", content); stateEvents.append(memberEvent); } diff --git a/room.h b/room.h index a0dad8b3..0ef17abb 100644 --- a/room.h +++ b/room.h @@ -160,6 +160,28 @@ namespace QMatrixClient */ Q_INVOKABLE QImage avatar(int width, int height); + /** + * @brief Get a user object for a given user id + * This is the recommended way to get a user object in a room + * context. The actual object type may be changed in further + * versions to provide room-specific user information (display name, + * avatar etc.). + * \note The method will return a valid user regardless of + * the membership. + */ + Q_INVOKABLE User* user(const QString& userId) const; + + /** + * \brief Check the join state of a given user in this room + * + * \note Banned and invited users are not tracked for now (Leave + * will be returned for them). + * + * \return either of Join, Leave, depending on the given + * user's state in the room + */ + Q_INVOKABLE JoinState memberJoinState(User* user) const; + /** * @brief Produces a disambiguated name for a given user in * the context of the room -- cgit v1.2.3 From c12f5cc213ecbb40c506e304e6b41c1437ca0d33 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Fri, 23 Feb 2018 11:16:10 +0900 Subject: Support per-room user traits User::Private stores two new maps (for display names and avatars respectively) and is able to retrieve and store names/avatars on a per-room basis. The "most used" trait is stored separately and room lists are not kept for it (because most people have a single name and a single avatar across all or most rooms). TODO: The avatar container should be replaced with something less clumsy; the current code doesn't even compile with MSVC. A "handle" copyable structure that would hold the Avatar is one of options. Closes #141. --- room.cpp | 28 +++--- user.cpp | 307 ++++++++++++++++++++++++++++++++++++++++++++++++++++----------- user.h | 36 ++++---- 3 files changed, 289 insertions(+), 82 deletions(-) (limited to 'room.cpp') diff --git a/room.cpp b/room.cpp index 3c6fb223..97f046d3 100644 --- a/room.cpp +++ b/room.cpp @@ -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(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")); 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 #include #include +#include #include +#include 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 otherNames; + std::vector>> otherAvatars; + + mutable int totalRooms = 0; + + QString nameForRoom(const Room* r) const; + std::pair 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 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(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( + 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(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(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); } diff --git a/user.h b/user.h index fa85d778..0a9d3da1 100644 --- a/user.h +++ b/user.h @@ -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 d; }; } Q_DECLARE_METATYPE(QMatrixClient::User*) -- cgit v1.2.3 From e09c740c85e2e9a861e84f706680ce1ec662a4bc Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sat, 24 Feb 2018 14:43:03 +0900 Subject: User/Room: signal that a user/member is about to change the name Enables to address QMatrixClient/Quaternion#284. Also fixes a gibberish condition in Room::Private::renameMember() that led to improper warnings and a too early return. --- room.cpp | 53 ++++++++++++++++++++++++++++++++++++----------------- room.h | 8 +++++--- user.cpp | 58 +++++++++++++++++++++++++++++++++------------------------- user.h | 7 ++++++- 4 files changed, 80 insertions(+), 46 deletions(-) (limited to 'room.cpp') diff --git a/room.cpp b/room.cpp index 97f046d3..06041090 100644 --- a/room.cpp +++ b/room.cpp @@ -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, const Room* context); + void renameMember(User* u, QString oldName); void removeMemberFromMap(const QString& username, User* u); void getPreviousContent(int limit = 10); @@ -721,42 +721,49 @@ int Room::timelineSize() const void Room::Private::insertMemberIntoMap(User *u) { 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 // for that other one because the two should be disambiguated now. + auto namesakes = membersMap.values(userName); + if (namesakes.size() == 1) + emit q->memberAboutToRename(namesakes.front(), + namesakes.front()->fullName(q)); + membersMap.insert(userName, u); if (namesakes.size() == 1) emit q->memberRenamed(namesakes.front()); } -void Room::Private::renameMember(User* u, QString oldName, const Room* context) +void Room::Private::renameMember(User* u, QString oldName) { - if (context != q) - return; // It's not a rename for this room - if (q->memberJoinState(u) == JoinState::Join) + if (u->name(q) == oldName) { qCWarning(MAIN) << "Room::Private::renameMember(): the user " << u->fullName(q) << "is already known in the room under a new name."; - return; } - - if (membersMap.contains(oldName, u)) + else if (membersMap.contains(oldName, u)) { removeMemberFromMap(oldName, u); insertMemberIntoMap(u); - emit q->memberRenamed(u); } + emit q->memberRenamed(u); } void Room::Private::removeMemberFromMap(const QString& username, User* u) { + User* namesake = nullptr; + auto namesakes = membersMap.values(username); + if (namesakes.size() == 2) + { + namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); + Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); + emit q->memberAboutToRename(namesake, username); + } 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 (membersMap.count(username) == 1) - emit q->memberRenamed(membersMap.value(username)); + if (namesake) + emit q->memberRenamed(namesake); } inline auto makeErrorStr(const Event& e, QByteArray msg) @@ -1327,8 +1334,16 @@ void Room::processStateEvents(const RoomEvents& events) if (memberJoinState(u) != JoinState::Join) { d->insertMemberIntoMap(u); + connect(u, &User::nameAboutToChange, this, + [=] (QString newName, QString, const Room* context) { + if (context == this) + emit memberAboutToRename(u, newName); + }); connect(u, &User::nameChanged, this, - std::bind(&Private::renameMember, d, u, _2, _3)); + [=] (QString, QString oldName, const Room* context) { + if (context == this) + d->renameMember(u, oldName); + }); emit userAdded(u); } } @@ -1602,12 +1617,16 @@ MemberSorter Room::memberSorter() const } bool MemberSorter::operator()(User *u1, User *u2) const +{ + return operator()(u1, room->roomMembername(u2)); +} + +bool MemberSorter::operator ()(User* u1, const QString& u2name) const { auto n1 = room->roomMembername(u1); - auto n2 = room->roomMembername(u2); if (n1.startsWith('@')) n1.remove(0, 1); - if (n2.startsWith('@')) - n2.remove(0, 1); + auto n2 = u2name.midRef(u2name.startsWith('@') ? 1 : 0); + return n1.localeAwareCompare(n2) < 0; } diff --git a/room.h b/room.h index 0ef17abb..5253a7c6 100644 --- a/room.h +++ b/room.h @@ -301,6 +301,7 @@ namespace QMatrixClient void avatarChanged(); void userAdded(User* user); void userRemoved(User* user); + void memberAboutToRename(User* user, QString newName); void memberRenamed(User* user); void memberListChanged(); @@ -345,12 +346,13 @@ namespace QMatrixClient explicit MemberSorter(const Room* r) : room(r) { } bool operator()(User* u1, User* u2) const; + bool operator()(User* u1, const QString& u2name) const; - template + template typename ContT::size_type lowerBoundIndex(const ContT& c, - typename ContT::value_type v) const + const ValT& v) const { - return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin(); + return std::lower_bound(c.begin(), c.end(), v, *this) - c.begin(); } private: diff --git a/user.cpp b/user.cpp index 2a3071af..8a9fa515 100644 --- a/user.cpp +++ b/user.cpp @@ -62,8 +62,8 @@ class User::Private mutable int totalRooms = 0; - QString nameForRoom(const Room* r) const; - std::pair setNameForRoom(const Room* r, QString newName); + QString nameForRoom(const Room* r, const QString& hint = {}) const; + void setNameForRoom(const Room* r, QString newName, QString oldName); const Avatar& avatarForRoom(const Room* r) const; bool setAvatarUrlForRoom(const Room* r, const QUrl& avatarUrl); @@ -78,34 +78,29 @@ QIcon User::Private::defaultIcon() return icon; } -QString User::Private::nameForRoom(const Room* r) const +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 = 100; +static constexpr int MIN_JOINED_ROOMS_TO_LOG = 20; -std::pair User::Private::setNameForRoom(const Room* r, - QString newName) +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"); - // 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); + mostUsedName = move(newName); + return; } + otherNames.remove(oldName, r); if (newName != mostUsedName) { // Check if the newName is about to become most used. @@ -134,7 +129,6 @@ std::pair User::Private::setNameForRoom(const Room* r, else otherNames.insert(newName, r); } - return { true, oldName }; } const Avatar& User::Private::avatarForRoom(const Room* r) const @@ -255,11 +249,19 @@ QString User::name(const Room* room) const void User::updateName(const QString& newName, const Room* room) { - const auto setNameResult = d->setNameForRoom(room, newName); - if (setNameResult.first) + updateName(newName, d->nameForRoom(room), room); +} + +void User::updateName(const QString& newName, const QString& oldName, + const Room* room) +{ + 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, setNameResult.second, room); + emit nameChanged(newName, oldName, room); } } @@ -337,7 +339,8 @@ QImage User::avatar(int width, int height, const Room* room) return avatar(width, height, room, []{}); } -QImage User::avatar(int width, int height, const Room* room, Avatar::get_callback_t callback) +QImage User::avatar(int width, int height, const Room* room, + Avatar::get_callback_t callback) { return avatarObject(room).get(d->connection, width, height, [=] { emit avatarChanged(this, room); callback(); }); @@ -385,7 +388,12 @@ void User::processEvent(RoomMemberEvent* event, const Room* room) } newName.truncate(match.capturedStart(0)); } - updateName(event->displayName(), room); + if (event->prevContent()) + updateName(event->displayName(), + d->nameForRoom(room, event->prevContent()->displayName), + room); + else + updateName(event->displayName(), room); if (d->setAvatarUrlForRoom(room, event->avatarUrl())) emit avatarChanged(this, room); } diff --git a/user.h b/user.h index 0a9d3da1..d1d28312 100644 --- a/user.h +++ b/user.h @@ -88,7 +88,8 @@ namespace QMatrixClient 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); + QImage avatar(int width, int height, const Room* room, + Avatar::get_callback_t callback); QString avatarMediaId(const Room* room = nullptr) const; QUrl avatarUrl(const Room* room = nullptr) const; @@ -102,12 +103,16 @@ namespace QMatrixClient bool setAvatar(QIODevice* source); signals: + void nameAboutToChange(QString newName, QString oldName, + const Room* roomContext); void nameChanged(QString newName, QString oldName, const Room* roomContext); void avatarChanged(User* user, const Room* roomContext); private slots: void updateName(const QString& newName, const Room* room = nullptr); + void updateName(const QString& newName, const QString& oldName, + const Room* room = nullptr); private: class Private; -- cgit v1.2.3 From e77a53946805649be99f8c0f6ee9c00702348132 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 25 Feb 2018 18:25:22 +0900 Subject: Room: show 1-on-1 invitations in a better way Previously it was just an "Empty room" name, now it's "Invitation from %1". --- room.cpp | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'room.cpp') diff --git a/room.cpp b/room.cpp index 06041090..762e929c 100644 --- a/room.cpp +++ b/room.cpp @@ -1452,6 +1452,12 @@ QString Room::Private::roomNameFromMemberNames(const QList &userlist) co } ); + // Spec extension. A single person in the chat but not the local user + // (the local user is apparently invited). + if (userlist.size() == 1 && !isLocalUser(first_two.front())) + return tr("Invitation from %1") + .arg(q->roomMembername(first_two.front())); + // i. One-on-one chat. first_two[1] == localUser() in this case. if (userlist.size() == 2) return q->roomMembername(first_two[0]); -- cgit v1.2.3 From ec412621d71b1a7758b15d2b3c5cd5e7b2081ab1 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Sun, 25 Feb 2018 18:57:02 +0900 Subject: Room and Connection: support room tags Closes #134. --- connection.cpp | 24 ++++++++++++++++++++++++ connection.h | 15 +++++++++++++++ room.cpp | 34 ++++++++++++++++++++++++++++++++++ room.h | 9 +++++++++ 4 files changed, 82 insertions(+) (limited to 'room.cpp') diff --git a/connection.cpp b/connection.cpp index 4b7d4abb..52fcc40b 100644 --- a/connection.cpp +++ b/connection.cpp @@ -520,6 +520,30 @@ QHash< QPair, Room* > Connection::roomMap() const return roomMap; } +QHash> Connection::tagsToRooms() const +{ + QHash> result; + for (auto* r: d->roomMap) + { + for (const auto& tagName: r->tagNames()) + result[tagName].push_back(r); + } + for (auto it = result.begin(); it != result.end(); ++it) + std::sort(it->begin(), it->end(), + [t=it.key()] (Room* r1, Room* r2) { + return r1->tags().value(t).order < r2->tags().value(t).order; + }); + return result; +} + +QVector Connection::roomsWithTag(const QString& tagName) const +{ + QVector rooms; + std::copy_if(d->roomMap.begin(), d->roomMap.end(), std::back_inserter(rooms), + [&tagName] (Room* r) { return r->tags().contains(tagName); }); + return rooms; +} + QMap Connection::users() const { return d->userMap; diff --git a/connection.h b/connection.h index 3ec4fd9d..b45a171d 100644 --- a/connection.h +++ b/connection.h @@ -70,7 +70,22 @@ namespace QMatrixClient explicit Connection(const QUrl& server, QObject* parent = nullptr); virtual ~Connection(); + /** Get all Invited and Joined rooms + * \return a hashmap from a composite key - room name and whether + * it's an Invite rather than Join - to room pointers + */ QHash, Room*> roomMap() const; + + /** Get all Invited and Joined rooms grouped by tag + * \return a hashmap from tag name to a vector of room pointers, + * sorted by their order in the tag - details are at + * https://matrix.org/speculator/spec/drafts%2Fe2e/client_server/unstable.html#id95 + */ + QHash> tagsToRooms() const; + + /** Get the list of rooms with the specified tag */ + QVector roomsWithTag(const QString& tagName) const; + QMap users() const; // FIXME: Convert Q_INVOKABLEs to Q_PROPERTIES diff --git a/room.cpp b/room.cpp index 762e929c..29244da2 100644 --- a/room.cpp +++ b/room.cpp @@ -95,6 +95,8 @@ class Room::Private QString firstDisplayedEventId; QString lastDisplayedEventId; QHash lastReadEventIds; + QHash tags; + QHash accountData; QString prevBatch; QPointer roomMessagesJob; @@ -552,6 +554,16 @@ void Room::resetHighlightCount() emit highlightCountChanged(this); } +QStringList Room::tagNames() const +{ + return d->tags.keys(); +} + +const QHash& Room::tags() const +{ + return d->tags; +} + const RoomMessageEvent* Room::Private::getEventWithFile(const QString& eventId) const { @@ -873,6 +885,15 @@ void Room::updateData(SyncRoomData&& data) << et.elapsed() << "ms"; } + if (!data.accountData.empty()) + { + et.restart(); + for (auto&& event: data.accountData) + processAccountDataEvent(move(event)); + qCDebug(PROFILER) << "*** Room::processAccountData():" + << et.elapsed() << "ms"; + } + if( data.highlightCount != d->highlightCount ) { d->highlightCount = data.highlightCount; @@ -1433,6 +1454,19 @@ void Room::processEphemeralEvent(EventPtr event) } } +void Room::processAccountDataEvent(EventPtr event) +{ + switch (event->type()) + { + case EventType::Tag: + d->tags = static_cast(event.get())->tags(); + emit tagsChanged(); + break; + default: + d->accountData[event->jsonType()] = event->contentJson(); + } +} + QString Room::Private::roomNameFromMemberNames(const QList &userlist) const { // This is part 3(i,ii,iii) in the room displayname algorithm described diff --git a/room.h b/room.h index 5253a7c6..6dba6156 100644 --- a/room.h +++ b/room.h @@ -20,6 +20,7 @@ #include "jobs/syncjob.h" #include "events/roommessageevent.h" +#include "events/tagevent.h" #include "joinstate.h" #include @@ -116,6 +117,8 @@ namespace QMatrixClient Q_PROPERTY(QString lastDisplayedEventId READ lastDisplayedEventId WRITE setLastDisplayedEventId NOTIFY lastDisplayedEventChanged) Q_PROPERTY(QString readMarkerEventId READ readMarkerEventId WRITE markMessagesAsRead NOTIFY readMarkerMoved) + Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) + public: using Timeline = std::deque; using rev_iter_t = Timeline::const_reverse_iterator; @@ -237,6 +240,9 @@ namespace QMatrixClient Q_INVOKABLE int highlightCount() const; Q_INVOKABLE void resetHighlightCount(); + QStringList tagNames() const; + const QHash& tags() const; + Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); Q_INVOKABLE QUrl urlToDownload(const QString& eventId); Q_INVOKABLE QString fileNameToDownload(const QString& eventId); @@ -318,6 +324,8 @@ namespace QMatrixClient void readMarkerMoved(); void unreadMessagesChanged(Room* room); + void tagsChanged(); + void replacedEvent(const RoomEvent* newEvent, const RoomEvent* oldEvent); @@ -330,6 +338,7 @@ namespace QMatrixClient protected: virtual void processStateEvents(const RoomEvents& events); virtual void processEphemeralEvent(EventPtr event); + virtual void processAccountDataEvent(EventPtr event); virtual void onAddNewTimelineEvents(timeline_iter_t from) { } virtual void onAddHistoricalTimelineEvents(rev_iter_t from) { } virtual void onRedaction(const RoomEvent* prevEvent, -- cgit v1.2.3 From f366cb25d1a5256341c1253fb04e36bd70373a70 Mon Sep 17 00:00:00 2001 From: Kitsune Ral Date: Tue, 27 Feb 2018 11:36:42 +0900 Subject: Room: Save tags in the cache; isFavourite() and isLowPriority() --- connection.cpp | 2 +- room.cpp | 29 +++++++++++++++++++++++++++++ room.h | 9 +++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) (limited to 'room.cpp') diff --git a/connection.cpp b/connection.cpp index 7b72c592..9a5a5a4e 100644 --- a/connection.cpp +++ b/connection.cpp @@ -627,7 +627,7 @@ void Connection::setHomeserver(const QUrl& url) emit homeserverChanged(homeserver()); } -static constexpr int CACHE_VERSION_MAJOR = 2; +static constexpr int CACHE_VERSION_MAJOR = 3; static constexpr int CACHE_VERSION_MINOR = 0; void Connection::saveState(const QUrl &toFile) const diff --git a/room.cpp b/room.cpp index 29244da2..db36a713 100644 --- a/room.cpp +++ b/room.cpp @@ -564,6 +564,21 @@ const QHash& Room::tags() const return d->tags; } +TagRecord Room::tag(const QString& name) const +{ + return d->tags.value(name); +} + +bool Room::isFavourite() const +{ + return d->tags.contains(FavouriteTag); +} + +bool Room::isLowPriority() const +{ + return d->tags.contains(LowPriorityTag); +} + const RoomMessageEvent* Room::Private::getEventWithFile(const QString& eventId) const { @@ -1635,6 +1650,20 @@ QJsonObject Room::Private::toJson() const result.insert("ephemeral", ephemeralObj); } + { + QJsonObject accountDataObj; + if (!tags.empty()) + { + QJsonObject tagsObj; + for (auto it = tags.begin(); it != tags.end(); ++it) + tagsObj.insert(it.key(), { {"order", it->order} }); + if (!tagsObj.empty()) + accountDataObj.insert("m.tag", tagsObj); + } + if (!accountDataObj.empty()) + result.insert("account_data", accountDataObj); + } + QJsonObject unreadNotificationsObj; if (highlightCount > 0) unreadNotificationsObj.insert("highlight_count", highlightCount); diff --git a/room.h b/room.h index 6dba6156..8e27a608 100644 --- a/room.h +++ b/room.h @@ -242,6 +242,15 @@ namespace QMatrixClient QStringList tagNames() const; const QHash& tags() const; + TagRecord tag(const QString& name) const; + + /** Check whether the list of tags has m.favourite */ + bool isFavourite() const; + /** Check whether the list of tags has m.lowpriority */ + bool isLowPriority() const; + + /** Check whether this room is a direct chat */ + bool isDirectChat() const; Q_INVOKABLE QUrl urlToThumbnail(const QString& eventId); Q_INVOKABLE QUrl urlToDownload(const QString& eventId); -- cgit v1.2.3