From a2a8ad09fd672cf70c8b1e3db0762abc5776b72e Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 21 Jul 2024 16:00:25 +0100 Subject: [PATCH] Create NeochatRoomMember as a shim for RoomMember so it can be safely passed to QML --- src/CMakeLists.txt | 2 + src/models/messageeventmodel.cpp | 10 +- src/neochatroommember.cpp | 169 +++++++++++++++++++++++++ src/neochatroommember.h | 204 +++++++++++++++++++++++++++++++ 4 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 src/neochatroommember.cpp create mode 100644 src/neochatroommember.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4a15d7bcb..0b9420d04 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -190,6 +190,8 @@ add_library(neochat STATIC threepidbindhelper.h models/readmarkermodel.cpp models/readmarkermodel.h + neochatroommember.cpp + neochatroommember.h ) set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES diff --git a/src/models/messageeventmodel.cpp b/src/models/messageeventmodel.cpp index 26eaf1b5a..d1a8bf640 100644 --- a/src/models/messageeventmodel.cpp +++ b/src/models/messageeventmodel.cpp @@ -26,6 +26,8 @@ #include "messagecontentmodel.h" #include "models/messagefiltermodel.h" #include "models/reactionmodel.h" +#include "neochatroom.h" +#include "neochatroommember.h" #include "readmarkermodel.h" #include "texthandler.h" @@ -469,7 +471,13 @@ QVariant MessageEventModel::data(const QModelIndex &idx, int role) const } if (role == AuthorRole) { - return QVariant::fromValue(eventHandler.getAuthor(isPending)); + QString mId; + if (isPending) { + mId = m_currentRoom->localMember().id(); + } else { + mId = evt.senderId(); + } + return QVariant::fromValue(new NeochatRoomMember(m_currentRoom, mId)); } if (role == HighlightRole) { diff --git a/src/neochatroommember.cpp b/src/neochatroommember.cpp new file mode 100644 index 000000000..f2cd18002 --- /dev/null +++ b/src/neochatroommember.cpp @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "neochatroommember.h" + +#include "neochatroom.h" + +NeochatRoomMember::NeochatRoomMember(NeoChatRoom *room, const QString &memberId) + : m_room(room) + , m_memberId(memberId) +{ + if (m_room != nullptr) { + connect(m_room, &NeoChatRoom::memberNameUpdated, this, [this](Quotient::RoomMember member) { + if (member.id() == m_memberId) { + Q_EMIT displayNameUpdated(); + } + }); + connect(m_room, &NeoChatRoom::memberAvatarUpdated, this, [this](Quotient::RoomMember member) { + if (member.id() == m_memberId) { + Q_EMIT avatarUpdated(); + } + }); + } +} + +bool NeochatRoomMember::operator==(const NeochatRoomMember &other) const +{ + return id() == other.id(); +} + +QString NeochatRoomMember::id() const +{ + return m_memberId; +} + +Quotient::Uri NeochatRoomMember::uri() const +{ + if (m_room == nullptr) { + return {}; + } + + return m_room->member(m_memberId).uri(); +} + +bool NeochatRoomMember::isLocalMember() const +{ + if (m_room == nullptr) { + return false; + } + + return m_room->member(m_memberId).isLocalMember(); +} + +Quotient::Membership NeochatRoomMember::membershipState() const +{ + if (m_room == nullptr) { + return Quotient::Membership::Leave; + } + + return m_room->member(m_memberId).membershipState(); +} + +QString NeochatRoomMember::name() const +{ + if (m_room == nullptr) { + return id(); + } + + return m_room->member(m_memberId).name(); +} + +QString NeochatRoomMember::displayName() const +{ + if (m_room == nullptr) { + return id(); + } + + return m_room->member(m_memberId).displayName(); +} + +QString NeochatRoomMember::htmlSafeDisplayName() const +{ + if (m_room == nullptr) { + return id(); + } + + return m_room->member(m_memberId).htmlSafeDisplayName(); +} + +QString NeochatRoomMember::fullName() const +{ + if (m_room == nullptr) { + return id(); + } + + return m_room->member(m_memberId).fullName(); +} + +QString NeochatRoomMember::htmlSafeFullName() const +{ + if (m_room == nullptr) { + return id(); + } + + return m_room->member(m_memberId).htmlSafeFullName(); +} + +QString NeochatRoomMember::disambiguatedName() const +{ + if (m_room == nullptr) { + return id(); + } + + return m_room->member(m_memberId).disambiguatedName(); +} + +QString NeochatRoomMember::htmlSafeDisambiguatedName() const +{ + if (m_room == nullptr) { + return id(); + } + + return m_room->member(m_memberId).htmlSafeDisambiguatedName(); +} + +int NeochatRoomMember::hue() const +{ + if (m_room == nullptr) { + return 0; + } + + return m_room->member(m_memberId).hue(); +} + +qreal NeochatRoomMember::hueF() const +{ + if (m_room == nullptr) { + return 0.0; + } + + return m_room->member(m_memberId).hueF(); +} + +QColor NeochatRoomMember::color() const +{ + if (m_room == nullptr) { + return {}; + } + + return m_room->member(m_memberId).color(); +} + +QString NeochatRoomMember::avatarMediaId() const +{ + if (m_room == nullptr) { + return {}; + } + + return m_room->member(m_memberId).avatarMediaId(); +} + +QUrl NeochatRoomMember::avatarUrl() const +{ + if (m_room == nullptr) { + return {}; + } + + return m_room->member(m_memberId).avatarUrl(); +} diff --git a/src/neochatroommember.h b/src/neochatroommember.h new file mode 100644 index 000000000..7cdeee49c --- /dev/null +++ b/src/neochatroommember.h @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include +#include + +#include +#include + +class NeoChatRoom; + +//! This class is for visualizing a user in a room context. +//! +//! The class is intentionally a read-only data object that is effectively a wrapper +//! around an m.room.member event for the desired user. This is designed provide the +//! data in a format ready for visualizing a user (avatar or name) in the context +//! of the room it was generated in. This means that if a user has set a unique +//! name or avatar for a particular room that is what will be returned. +//! +//! \note The RoomMember class is not intended for interacting with a User's profile. +//! For that a Quotient::User object should be obtained from a +//! Quotient::Connection as that has the support functions for modifying profile +//! information. +//! +//! \sa Quotient::User +class NeochatRoomMember : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + + Q_PROPERTY(QString id READ id CONSTANT) + Q_PROPERTY(Quotient::Uri uri READ uri CONSTANT) + Q_PROPERTY(bool isLocalMember READ isLocalMember CONSTANT) + Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameUpdated) + Q_PROPERTY(QString htmlSafeDisplayName READ htmlSafeDisplayName NOTIFY displayNameUpdated) + Q_PROPERTY(QString fullName READ fullName NOTIFY displayNameUpdated) + Q_PROPERTY(QString htmlSafeFullName READ htmlSafeFullName NOTIFY displayNameUpdated) + Q_PROPERTY(QString disambiguatedName READ disambiguatedName NOTIFY displayNameUpdated) + Q_PROPERTY(QString htmlSafeDisambiguatedName READ htmlSafeDisambiguatedName NOTIFY displayNameUpdated) + Q_PROPERTY(int hue READ hue CONSTANT) + Q_PROPERTY(qreal hueF READ hueF CONSTANT) + Q_PROPERTY(QColor color READ color CONSTANT) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarUpdated) + +public: + NeochatRoomMember() = default; + + explicit NeochatRoomMember(NeoChatRoom *room, const QString &memberId); + + bool operator==(const NeochatRoomMember &other) const; + + /** + * @brief Get unique stable user id + * + * The Matrix user ID is generated by the server and is never changed. + */ + QString id() const; + + /** + * @brief The matrix.to URI for the user + * + * Typically used when you want to visit a user (see + * Quotient::UriResolverBase::visitResource()). + * + * @sa Quotient::UriResolverBase::visitResource() + */ + Quotient::Uri uri() const; + + //! Whether this member is the local user + bool isLocalMember() const; + + //! The membership state of the member + Quotient::Membership membershipState() const; + + //! \brief The raw unmodified display name for the user in the given room + //! + //! The value will be empty if no display name has been set. + //! + //! \warning This value is not sanitized or HTML escape so use appropriately. + //! For ready to display values use displayName() or fullName() for + //! plain text and htmlSafeDisplayName() or htmlSafeFullName() fo + //! rich text. + //! + //! \sa displayName(), htmlSafeDisplayName(), fullName(), htmlSafeFullName() + QString name() const; + + //! \brief Get the user display name ready for display + //! + //! This function always aims to return something that can be displayed in a + //! UI, so if no display name is set the user's Matrix ID will be returned. + //! + //! The output is sanitized and suitable for a plain text field. For a rich + //! field use htmlSafeDisplayName(). + //! + //! \sa htmlSafeDisplayName() + QString displayName() const; + + //! \brief Get the user display name ready for display + //! + //! This function always aims to return something that can be displayed in a + //! UI, so if no display name is set the user's Matrix ID will be returned. + //! + //! The output is sanitized and html escaped ready for a rich text field. For + //! a plain field use displayName(). + //! + //! \sa displayName() + QString htmlSafeDisplayName() const; + + //! \brief Get user name and id in a single string + //! + //! This function always aims to return something that can be displayed in a + //! UI, so if no display name is set the just user's Matrix ID will be returned. + //! The constructed string follows the format 'name (id)' which the spec + //! recommends for users disambiguation in a room context and in other places. + //! + //! The output is sanitized and suitable for a plain text field. For a rich + //! field use htmlSafeFullName(). + //! + //! \sa htmlSafeFullName() + QString fullName() const; + + //! \brief Get user name and id in a single string + //! + //! This function always aims to return something that can be displayed in a + //! UI, so if no display name is set the just user's Matrix ID will be returned. + //! The constructed string follows the format 'name (id)' which the spec + //! recommends for users disambiguation in a room context and in other places. + //! + //! The output is sanitized and html escaped ready for a rich text field. For + //! a plain field use fullName(). + //! + //! \sa fullName() + QString htmlSafeFullName() const; + + //! \brief Get the disambiguated user name + //! + //! This function always aims to return something that can be displayed in a + //! UI, so if no display name is set the just user's Matrix ID will be returned. + //! The output is equivalent to fullName() if there is another user in the room + //! with the same name. Otherwise it is equivalent to displayName(). + //! + //! The output is sanitized and suitable for a plain text field. For a rich + //! field use htmlSafeDisambiguatedName(). + //! + //! \sa htmlSafeDisambiguatedName(), fullName(), displayName() + QString disambiguatedName() const; + + //! \brief Get the disambiguated user name + //! + //! This function always aims to return something that can be displayed in a + //! UI, so if no display name is set the just user's Matrix ID will be returned. + //! The output is equivalent to htmlSafeFullName() if there is another user in the room + //! with the same name. Otherwise it is equivalent to htmlSafeDisplayName(). + //! + //! The output is sanitized and html escaped ready for a rich text field. For + //! a plain field use disambiguatedName(). + //! + //! \sa disambiguatedName(), htmlSafeFullName(), htmlSafeDisplayName() + QString htmlSafeDisambiguatedName() const; + + //! \brief Hue color component of this user based on the user's Matrix ID + //! + //! The implementation is based on XEP-0392: + //! https://xmpp.org/extensions/xep-0392.html + //! Naming and ranges are the same as QColor's hue methods: + //! https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision + int hue() const; + + //! \brief HueF color component of this user based on the user's Matrix ID + //! + //! The implementation is based on XEP-0392: + //! https://xmpp.org/extensions/xep-0392.html + //! Naming and ranges are the same as QColor's hue methods: + //! https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision + qreal hueF() const; + + //! \brief Color based on the user's Matrix ID + //! + //! See https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal + //! for the methodology. + QColor color() const; + + //! \brief The mxc URL as a string for the user avatar in the room + //! + //! This can be empty if none set. + QString avatarMediaId() const; + + //! \brief The mxc URL for the user avatar in the room + //! + //! This can be empty if none set. + QUrl avatarUrl() const; + +Q_SIGNALS: + void displayNameUpdated(); + void avatarUpdated(); + +private: + QPointer m_room; + const QString m_memberId; +};