diff --git a/CMakeLists.txt b/CMakeLists.txt index 20ef5cab..1b6c08b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -345,6 +345,7 @@ set(SRC_FILES src/DeviceVerificationFlow.cpp src/EventAccessors.cpp src/InviteesModel.cpp + src/JdenticonProvider.cpp src/Logging.cpp src/LoginPage.cpp src/MainWindow.cpp @@ -557,6 +558,7 @@ qt5_wrap_cpp(MOC_HEADERS src/DeviceVerificationFlow.h src/ImagePackListModel.h src/InviteesModel.h + src/JdenticonProvider.h src/LoginPage.h src/MainWindow.h src/MemberList.h diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index ab067eee..5d2b583a 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -12,6 +12,7 @@ Rectangle { property string url property string userid + property string roomid property string displayName property alias textColor: label.color property bool crop: true @@ -35,10 +36,28 @@ Rectangle { font.pixelSize: avatar.height / 2 verticalAlignment: Text.AlignVCenter horizontalAlignment: Text.AlignHCenter - visible: img.status != Image.Ready + visible: img.status != Image.Ready && !Settings.useIdenticon color: Nheko.colors.text } + Image { + id: identicon + anchors.fill: parent + visible: Settings.useIdenticon && img.status != Image.Ready + source: Settings.useIdenticon ? ("image://jdenticon/" + (userid !== "" ? userid : roomid) + "?radius=" + (Settings.avatarCircles ? 100 : 25)) : "" + + MouseArea { + anchors.fill: parent + + Ripple { + rippleTarget: parent + color: Qt.rgba(Nheko.colors.alternateBase.r, Nheko.colors.alternateBase.g, Nheko.colors.alternateBase.b, 0.5) + } + + } + + } + Image { id: img diff --git a/resources/qml/CommunitiesList.qml b/resources/qml/CommunitiesList.qml index 491913be..ff9b7da7 100644 --- a/resources/qml/CommunitiesList.qml +++ b/resources/qml/CommunitiesList.qml @@ -130,6 +130,7 @@ Page { else return "image://colorimage/" + model.avatarUrl + "?" + communityItem.unimportantText; } + roomid: model.id displayName: model.displayName color: communityItem.background } diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index 00fc3216..6bde67fa 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -139,6 +139,7 @@ Popup { height: popup.avatarHeight width: popup.avatarWidth displayName: model.displayName + userid: model.userid url: model.avatarUrl.replace("mxc://", "image://MxcImage/") onClicked: popup.completionClicked(completer.completionAt(model.index)) } @@ -194,6 +195,7 @@ Popup { height: popup.avatarHeight width: popup.avatarWidth displayName: model.roomName + roomid: model.roomid url: model.avatarUrl.replace("mxc://", "image://MxcImage/") onClicked: { popup.completionClicked(completer.completionAt(model.index)); @@ -225,6 +227,7 @@ Popup { height: popup.avatarHeight width: popup.avatarWidth displayName: model.roomName + roomid: model.roomid url: model.avatarUrl.replace("mxc://", "image://MxcImage/") onClicked: popup.completionClicked(completer.completionAt(model.index)) } diff --git a/resources/qml/RoomDirectory.qml b/resources/qml/RoomDirectory.qml index 2d7b3a34..b51c7bbc 100644 --- a/resources/qml/RoomDirectory.qml +++ b/resources/qml/RoomDirectory.qml @@ -65,6 +65,7 @@ ApplicationWindow { width: avatarSize height: avatarSize url: model.avatarUrl.replace("mxc://", "image://MxcImage/") + roomid: model.roomid displayName: model.name } diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index a0009174..addbf571 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -143,6 +143,8 @@ Page { required property int notificationCount required property bool hasLoudNotification required property bool hasUnreadMessages + required property bool isDirect + required property string directChatOtherUserId color: background height: avatarSize + 2 * Nheko.paddingMedium @@ -237,6 +239,8 @@ Page { width: avatarSize url: avatarUrl.replace("mxc://", "image://MxcImage/") displayName: roomName + userid: isDirect ? directChatOtherUserId : "" + roomid: roomId Rectangle { id: collapsedNotificationBubble diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml index 62175bf0..3376a4b6 100644 --- a/resources/qml/RoomMembers.qml +++ b/resources/qml/RoomMembers.qml @@ -39,6 +39,7 @@ ApplicationWindow { width: 130 height: width + roomid: members.roomId displayName: members.roomName Layout.alignment: Qt.AlignHCenter url: members.avatarUrl.replace("mxc://", "image://MxcImage/") diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml index a70cd71a..152567c8 100644 --- a/resources/qml/RoomSettings.qml +++ b/resources/qml/RoomSettings.qml @@ -38,6 +38,7 @@ ApplicationWindow { Avatar { url: roomSettings.roomAvatarUrl.replace("mxc://", "image://MxcImage/") + roomid: roomSettings.roomid displayName: roomSettings.roomName height: 130 width: 130 diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index f12060f2..91bbca5b 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -137,6 +137,7 @@ Item { ColumnLayout { id: preview + property string roomId: room ? room.roomId : (roomPreview ? roomPreview.roomId : "") property string roomName: room ? room.roomName : (roomPreview ? roomPreview.roomName : "") property string roomTopic: room ? room.roomTopic : (roomPreview ? roomPreview.roomTopic : "") property string avatarUrl: room ? room.roomAvatarUrl : (roomPreview ? roomPreview.roomAvatarUrl : "") @@ -153,6 +154,7 @@ Item { Avatar { url: parent.avatarUrl.replace("mxc://", "image://MxcImage/") + roomid: parent.roomId displayName: parent.roomName height: 130 width: 130 diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 7f67c028..05c61d99 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -13,10 +13,13 @@ Rectangle { property bool showBackButton: false property string roomName: room ? room.roomName : qsTr("No room selected") + property string roomId: room ? room.roomId : "" property string avatarUrl: room ? room.roomAvatarUrl : "" property string roomTopic: room ? room.roomTopic : "" property bool isEncrypted: room ? room.isEncrypted : false property int trustlevel: room ? room.trustlevel : Crypto.Unverified + property bool isDirect: room ? room.isDirect : false + property string directChatOtherUserId: room ? room.directChatOtherUserId : "" Layout.fillWidth: true implicitHeight: topLayout.height + Nheko.paddingMedium * 2 @@ -65,10 +68,12 @@ Rectangle { width: Nheko.avatarSize height: Nheko.avatarSize url: avatarUrl.replace("mxc://", "image://MxcImage/") + roomid: roomId + userid: isDirect ? directChatOtherUserId : "" displayName: roomName onClicked: { if (room) - TimelineManager.openRoomSettings(room.roomId); + TimelineManager.openRoomSettings(roomId); } } @@ -135,7 +140,7 @@ Rectangle { Platform.MenuItem { visible: room ? room.permissions.canInvite() : false text: qsTr("Invite users") - onTriggered: TimelineManager.openInviteUsers(room.roomId) + onTriggered: TimelineManager.openInviteUsers(roomId) } Platform.MenuItem { @@ -145,12 +150,12 @@ Rectangle { Platform.MenuItem { text: qsTr("Leave room") - onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId) + onTriggered: TimelineManager.openLeaveRoomDialog(roomId) } Platform.MenuItem { text: qsTr("Settings") - onTriggered: TimelineManager.openRoomSettings(room.roomId) + onTriggered: TimelineManager.openRoomSettings(roomId) } } diff --git a/resources/qml/components/AvatarListTile.qml b/resources/qml/components/AvatarListTile.qml index 36c26a97..853266c6 100644 --- a/resources/qml/components/AvatarListTile.qml +++ b/resources/qml/components/AvatarListTile.qml @@ -23,6 +23,8 @@ Rectangle { required property int index required property int selectedIndex property bool crop: true + property alias roomid: avatar.roomid + property alias userid: avatar.userid color: background height: avatarSize + 2 * Nheko.paddingMedium diff --git a/resources/qml/dialogs/ImagePackEditorDialog.qml b/resources/qml/dialogs/ImagePackEditorDialog.qml index e78213e0..c028f4a2 100644 --- a/resources/qml/dialogs/ImagePackEditorDialog.qml +++ b/resources/qml/dialogs/ImagePackEditorDialog.qml @@ -61,6 +61,7 @@ ApplicationWindow { header: AvatarListTile { title: imagePack.packname avatarUrl: imagePack.avatarUrl + roomid: imagePack.statekey subtitle: imagePack.statekey index: -1 selectedIndex: currentImageIndex @@ -142,6 +143,7 @@ ApplicationWindow { Layout.columnSpan: 2 url: imagePack.avatarUrl.replace("mxc://", "image://MxcImage/") displayName: imagePack.packname + roomid: imagePack.statekey height: 130 width: 130 crop: false @@ -219,6 +221,7 @@ ApplicationWindow { Layout.columnSpan: 2 url: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.Url).replace("mxc://", "image://MxcImage/") displayName: imagePack.data(imagePack.index(currentImageIndex, 0), SingleImagePackModel.ShortCode) + roomid: displayName height: 130 width: 130 crop: false diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml index b217abdd..ca09ff27 100644 --- a/resources/qml/dialogs/ImagePackSettingsDialog.qml +++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml @@ -112,6 +112,7 @@ ApplicationWindow { return qsTr("Globally enabled pack"); } selectedIndex: currentPackIndex + roomid: currentPack.statekey TapHandler { onSingleTapped: currentPackIndex = index @@ -135,6 +136,7 @@ ApplicationWindow { property string packName: currentPack ? currentPack.packname : "" property string attribution: currentPack ? currentPack.attribution : "" property string avatarUrl: currentPack ? currentPack.avatarUrl : "" + property string statekey: currentPack ? currentPack.statekey : "" anchors.fill: parent anchors.margins: Nheko.paddingLarge @@ -143,6 +145,7 @@ ApplicationWindow { Avatar { url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/") displayName: packinfo.packName + roomid: packinfo.statekey height: 100 width: 100 Layout.alignment: Qt.AlignHCenter diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml index d44c5edf..be698356 100644 --- a/resources/qml/voip/ActiveCallBar.qml +++ b/resources/qml/voip/ActiveCallBar.qml @@ -34,14 +34,15 @@ Rectangle { width: Nheko.avatarSize height: Nheko.avatarSize url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") - displayName: CallManager.callParty + userid: CallManager.callParty + displayName: CallManager.callPartyDisplayName onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) } Label { Layout.leftMargin: 8 font.pointSize: fontMetrics.font.pointSize * 1.1 - text: CallManager.callParty + text: CallManager.callPartyDisplayName color: "#000000" } diff --git a/resources/qml/voip/CallInvite.qml b/resources/qml/voip/CallInvite.qml index 253fa25c..1bd5eb26 100644 --- a/resources/qml/voip/CallInvite.qml +++ b/resources/qml/voip/CallInvite.qml @@ -40,7 +40,7 @@ Popup { Label { Layout.alignment: Qt.AlignCenter Layout.topMargin: msgView.height / 25 - text: CallManager.callParty + text: CallManager.callPartyDisplayName font.pointSize: fontMetrics.font.pointSize * 2 color: Nheko.colors.windowText } @@ -50,7 +50,8 @@ Popup { width: msgView.height / 5 height: msgView.height / 5 url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") - displayName: CallManager.callParty + userid: CallManager.callParty + displayName: CallManager.callPartyDisplayName } ColumnLayout { diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml index f6c1ecde..10f8367a 100644 --- a/resources/qml/voip/CallInviteBar.qml +++ b/resources/qml/voip/CallInviteBar.qml @@ -41,14 +41,15 @@ Rectangle { width: Nheko.avatarSize height: Nheko.avatarSize url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") - displayName: CallManager.callParty + userid: CallManager.callParty + displayName: CallManager.callPartyDisplayName onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) } Label { Layout.leftMargin: 8 font.pointSize: fontMetrics.font.pointSize * 1.1 - text: CallManager.callParty + text: CallManager.callPartyDisplayName color: "#000000" } diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml index 97932cc9..c733012c 100644 --- a/resources/qml/voip/PlaceCall.qml +++ b/resources/qml/voip/PlaceCall.qml @@ -79,6 +79,7 @@ Popup { height: Nheko.avatarSize url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") displayName: room.roomName + roomid: room.roomid onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) } diff --git a/src/Cache.cpp b/src/Cache.cpp index 84e2ddc2..9ebc61b9 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -2776,6 +2776,46 @@ Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_ return members; } +std::vector +Cache::getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len) +{ + auto txn = ro_txn(env_); + auto db = getInviteMembersDb(txn, room_id); + auto cursor = lmdb::cursor::open(txn, db); + + std::size_t currentIndex = 0; + + const auto endIndex = std::min(startIndex + len, db.size(txn)); + + std::vector members; + + std::string_view user_id, user_data; + while (cursor.get(user_id, user_data, MDB_NEXT)) { + if (currentIndex < startIndex) { + currentIndex += 1; + continue; + } + + if (currentIndex >= endIndex) + break; + + try { + MemberInfo tmp = json::parse(user_data); + members.emplace_back( + RoomMember{QString::fromStdString(std::string(user_id)), + QString::fromStdString(tmp.name)}); + } catch (const json::exception &e) { + nhlog::db()->warn("{}", e.what()); + } + + currentIndex += 1; + } + + cursor.close(); + + return members; +} + bool Cache::isRoomMember(const std::string &user_id, const std::string &room_id) { @@ -4808,6 +4848,12 @@ getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len) return instance_->getMembers(room_id, startIndex, len); } +std::vector +getMembersFromInvite(const std::string &room_id, std::size_t startIndex, std::size_t len) +{ + return instance_->getMembersFromInvite(room_id, startIndex, len); +} + void saveState(const mtx::responses::Sync &res) { diff --git a/src/Cache.h b/src/Cache.h index 57a36d73..f8626430 100644 --- a/src/Cache.h +++ b/src/Cache.h @@ -83,6 +83,9 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb); //! Retrieve member info from a room. std::vector getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30); +//! Retrive member info from an invite. +std::vector +getMembersFromInvite(const std::string &room_id, std::size_t start_index = 0, std::size_t len = 30); bool isInitialized(); diff --git a/src/CacheStructs.h b/src/CacheStructs.h index 4a5c5c76..5f4d392a 100644 --- a/src/CacheStructs.h +++ b/src/CacheStructs.h @@ -93,7 +93,7 @@ to_json(nlohmann::json &j, const RoomInfo &info); void from_json(const nlohmann::json &j, RoomInfo &info); -//! Basic information per member; +//! Basic information per member. struct MemberInfo { std::string name; diff --git a/src/Cache_p.h b/src/Cache_p.h index 7780c80f..43644d7e 100644 --- a/src/Cache_p.h +++ b/src/Cache_p.h @@ -106,6 +106,10 @@ public: std::vector getMembers(const std::string &room_id, std::size_t startIndex = 0, std::size_t len = 30); + + std::vector getMembersFromInvite(const std::string &room_id, + std::size_t startIndex = 0, + std::size_t len = 30); size_t memberCount(const std::string &room_id); void saveState(const mtx::responses::Sync &res); @@ -132,6 +136,9 @@ public: //! Retrieve all the user ids from a room. std::vector roomMembers(const std::string &room_id); + //! Get the other user from an invite to a direct chat. + RoomMember getDirectInviteMember(const std::string &room_id); + //! Check if the given user has power leve greater than than //! lowest power level of the given events. bool hasEnoughPowerLevel(const std::vector &eventTypes, @@ -310,7 +317,6 @@ public: return get_skey(a).compare(get_skey(b)); } - signals: void newReadReceipts(const QString &room_id, const std::vector &event_ids); void roomReadStatus(const std::map &status); diff --git a/src/CallManager.cpp b/src/CallManager.cpp index 6d41f1c6..601c9d6b 100644 --- a/src/CallManager.cpp +++ b/src/CallManager.cpp @@ -206,7 +206,9 @@ CallManager::sendInvite(const QString &roomid, CallType callType, unsigned int w std::vector members(cache::getMembers(roomid.toStdString())); const RoomMember &callee = members.front().user_id == utils::localUser() ? members.back() : members.front(); - callParty_ = callee.display_name.isEmpty() ? callee.user_id : callee.display_name; + callParty_ = callee.user_id; + callPartyDisplayName_ = + callee.display_name.isEmpty() ? callee.user_id : callee.display_name; callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); emit newInviteState(); playRingtone(QUrl("qrc:/media/media/ringback.ogg"), true); @@ -308,7 +310,9 @@ CallManager::handleEvent(const RoomEvent &callInviteEvent) std::vector members(cache::getMembers(callInviteEvent.room_id)); const RoomMember &caller = members.front().user_id == utils::localUser() ? members.back() : members.front(); - callParty_ = caller.display_name.isEmpty() ? caller.user_id : caller.display_name; + callParty_ = caller.user_id; + callPartyDisplayName_ = + caller.display_name.isEmpty() ? caller.user_id : caller.display_name; callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url); haveCallInvite_ = true; @@ -459,6 +463,7 @@ CallManager::clear() { roomid_.clear(); callParty_.clear(); + callPartyDisplayName_.clear(); callPartyAvatarUrl_.clear(); callid_.clear(); callType_ = CallType::VOICE; diff --git a/src/CallManager.h b/src/CallManager.h index 1d973191..407b8366 100644 --- a/src/CallManager.h +++ b/src/CallManager.h @@ -32,6 +32,7 @@ class CallManager : public QObject Q_PROPERTY(webrtc::CallType callType READ callType NOTIFY newInviteState) Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState) Q_PROPERTY(QString callParty READ callParty NOTIFY newInviteState) + Q_PROPERTY(QString callPartyDisplayName READ callPartyDisplayName NOTIFY newInviteState) Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY newInviteState) Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged) Q_PROPERTY(bool haveLocalPiP READ haveLocalPiP NOTIFY newCallState) @@ -48,6 +49,7 @@ public: webrtc::CallType callType() const { return callType_; } webrtc::State callState() const { return session_.state(); } QString callParty() const { return callParty_; } + QString callPartyDisplayName() const { return callPartyDisplayName_; } QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; } bool isMicMuted() const { return session_.isMicMuted(); } bool haveLocalPiP() const { return session_.haveLocalPiP(); } @@ -87,6 +89,7 @@ private: WebRTCSession &session_; QString roomid_; QString callParty_; + QString callPartyDisplayName_; QString callPartyAvatarUrl_; std::string callid_; const uint32_t timeoutms_ = 120000; diff --git a/src/JdenticonProvider.cpp b/src/JdenticonProvider.cpp new file mode 100644 index 00000000..3b819c7c --- /dev/null +++ b/src/JdenticonProvider.cpp @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "JdenticonProvider.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "Cache.h" +#include "Logging.h" +#include "MatrixClient.h" +#include "Utils.h" +#include "jdenticoninterface.h" + +static QPixmap +clipRadius(QPixmap img, double radius) +{ + QPixmap out(img.size()); + out.fill(Qt::transparent); + + QPainter painter(&out); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + + QPainterPath ppath; + ppath.addRoundedRect(img.rect(), radius, radius, Qt::SizeMode::RelativeSize); + + painter.setClipPath(ppath); + painter.drawPixmap(img.rect(), img); + + return out; +} + +JdenticonResponse::JdenticonResponse(const QString &key, + bool crop, + double radius, + const QSize &requestedSize) + : m_key(key) + , m_crop{crop} + , m_radius{radius} + , m_requestedSize(requestedSize.isValid() ? requestedSize : QSize(100, 100)) + , m_pixmap{m_requestedSize} + , jdenticonInterface_{Jdenticon::getJdenticonInterface()} +{ + setAutoDelete(false); +} + +void +JdenticonResponse::run() +{ + m_pixmap.fill(Qt::transparent); + + QPainter painter; + painter.begin(&m_pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + + QSvgRenderer renderer{ + jdenticonInterface_->generate(m_key, m_requestedSize.width()).toUtf8()}; + renderer.render(&painter); + + painter.end(); + + m_pixmap = clipRadius(m_pixmap, m_radius); + + emit finished(); +} + +namespace Jdenticon { +JdenticonInterface * +getJdenticonInterface() +{ + static JdenticonInterface *interface = nullptr; + static bool interfaceExists{true}; + + if (interface == nullptr && interfaceExists) { + QDir pluginsDir(qApp->applicationDirPath()); + + bool plugins = pluginsDir.cd("plugins"); + if (plugins) { + for (const QString &fileName : pluginsDir.entryList(QDir::Files)) { + QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName)); + QObject *plugin = pluginLoader.instance(); + if (plugin) { + interface = qobject_cast(plugin); + if (interface) { + nhlog::ui()->info("Loaded jdenticon plugin."); + break; + } + } + } + } else { + nhlog::ui()->info("jdenticon plugin not found."); + interfaceExists = false; + } + } + + return interface; +} +} diff --git a/src/JdenticonProvider.h b/src/JdenticonProvider.h new file mode 100644 index 00000000..bcda29c8 --- /dev/null +++ b/src/JdenticonProvider.h @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include + +#include "jdenticoninterface.h" + +namespace Jdenticon { +JdenticonInterface * +getJdenticonInterface(); +} + +class JdenticonResponse + : public QQuickImageResponse + , public QRunnable +{ +public: + JdenticonResponse(const QString &key, bool crop, double radius, const QSize &requestedSize); + + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_pixmap.toImage()); + } + + void run() override; + + QString m_key; + bool m_crop; + double m_radius; + QSize m_requestedSize; + QPixmap m_pixmap; + JdenticonInterface *jdenticonInterface_ = nullptr; +}; + +class JdenticonProvider + : public QObject + , public QQuickAsyncImageProvider +{ + Q_OBJECT + +public: + static bool isAvailable() { return Jdenticon::getJdenticonInterface() != nullptr; } + +public slots: + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override + { + auto id_ = id; + bool crop = true; + double radius = 0; + + auto queryStart = id.lastIndexOf('?'); + if (queryStart != -1) { + id_ = id.left(queryStart); + auto query = id.midRef(queryStart + 1); + auto queryBits = query.split('&'); + + for (auto b : queryBits) { + if (b.startsWith("radius=")) { + radius = b.mid(7).toDouble(); + } + } + } + + JdenticonResponse *response = + new JdenticonResponse(id_, crop, radius, requestedSize); + pool.start(response); + return response; + } + +private: + QThreadPool pool; +}; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 7eadc6df..b423304f 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -16,6 +16,7 @@ #include "Cache_p.h" #include "ChatPage.h" #include "Config.h" +#include "JdenticonProvider.h" #include "Logging.h" #include "LoginPage.h" #include "MainWindow.h" @@ -152,10 +153,6 @@ MainWindow::MainWindow(QWidget *parent) showChatPage(); } }); - - if (loadJdenticonPlugin()) { - nhlog::ui()->info("loaded jdenticon."); - } } void @@ -428,29 +425,6 @@ MainWindow::showDialog(QWidget *dialog) dialog->show(); } -bool -MainWindow::loadJdenticonPlugin() -{ - QDir pluginsDir(qApp->applicationDirPath()); - - bool plugins = pluginsDir.cd("plugins"); - if (plugins) { - foreach (QString fileName, pluginsDir.entryList(QDir::Files)) { - QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(fileName)); - QObject *plugin = pluginLoader.instance(); - if (plugin) { - jdenticonInteface_ = qobject_cast(plugin); - if (jdenticonInteface_) { - nhlog::ui()->info("Found jdenticon plugin."); - return true; - } - } - } - } - - nhlog::ui()->info("jdenticon plugin not found."); - return false; -} void MainWindow::showWelcomePage() { diff --git a/src/MainWindow.h b/src/MainWindow.h index d423af9f..d9ffb9b1 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -106,8 +106,6 @@ signals: void reload(); private: - bool loadJdenticonPlugin(); - void showDialog(QWidget *dialog); bool hasActiveUser(); void restoreWindowSize(); @@ -137,6 +135,4 @@ private: //! Overlay modal used to project other widgets. OverlayModal *modal_ = nullptr; LoadingIndicator *spinner_ = nullptr; - - JdenticonInterface *jdenticonInteface_ = nullptr; }; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index af32344c..7b01b0b8 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -86,6 +86,7 @@ UserSettings::load(std::optional profile) theme_ = settings.value("user/theme", defaultTheme_).toString(); font_ = settings.value("user/font_family", "default").toString(); avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); + useIdenticon_ = settings.value("user/use_identicon", true).toBool(); decryptSidebar_ = settings.value("user/decrypt_sidebar", true).toBool(); privacyScreen_ = settings.value("user/privacy_screen", false).toBool(); privacyScreenTimeout_ = settings.value("user/privacy_screen_timeout", 0).toInt(); @@ -596,6 +597,15 @@ UserSettings::setDisableCertificateValidation(bool disabled) disableCertificateValidation_ = disabled; http::client()->verify_certificates(!disabled); emit disableCertificateValidationChanged(disabled); +} + +void +UserSettings::setUseIdenticon(bool state) +{ + if (state == useIdenticon_) + return; + useIdenticon_ = state; + emit useIdenticonChanged(useIdenticon_); save(); } @@ -674,6 +684,7 @@ UserSettings::save() settings.setValue("screen_share_hide_cursor", screenShareHideCursor_); settings.setValue("use_stun_server", useStunServer_); settings.setValue("currentProfile", profile_); + settings.setValue("use_identicon", useIdenticon_); settings.endGroup(); // user @@ -746,6 +757,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge trayToggle_ = new Toggle{this}; startInTrayToggle_ = new Toggle{this}; avatarCircles_ = new Toggle{this}; + useIdenticon_ = new Toggle{this}; decryptSidebar_ = new Toggle(this); privacyScreen_ = new Toggle{this}; onlyShareKeysWithVerifiedUsers_ = new Toggle(this); @@ -779,6 +791,7 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge trayToggle_->setChecked(settings_->tray()); startInTrayToggle_->setChecked(settings_->startInTray()); avatarCircles_->setChecked(settings_->avatarCircles()); + useIdenticon_->setChecked(settings_->useIdenticon()); decryptSidebar_->setChecked(settings_->decryptSidebar()); privacyScreen_->setChecked(settings_->privacyScreen()); onlyShareKeysWithVerifiedUsers_->setChecked(settings_->onlyShareKeysWithVerifiedUsers()); @@ -941,6 +954,9 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge boxWrap(tr("Circular Avatars"), avatarCircles_, tr("Change the appearance of user avatars in chats.\nOFF - square, ON - Circle.")); + boxWrap(tr("Use identicons"), + useIdenticon_, + tr("Display an identicon instead of a letter when a user has not set an avatar.")); boxWrap(tr("Group's sidebar"), groupViewToggle_, tr("Show a column containing groups and tags next to the room list.")); @@ -1263,6 +1279,13 @@ UserSettingsPage::UserSettingsPage(QSharedPointer settings, QWidge settings_->setAvatarCircles(enabled); }); + if (JdenticonProvider::isAvailable()) + connect(useIdenticon_, &Toggle::toggled, this, [this](bool enabled) { + settings_->setUseIdenticon(enabled); + }); + else + useIdenticon_->setDisabled(true); + connect(markdown_, &Toggle::toggled, this, [this](bool enabled) { settings_->setMarkdown(enabled); }); diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 93b53211..bcd9439b 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -12,6 +12,7 @@ #include #include +#include "JdenticonProvider.h" #include class Toggle; @@ -105,6 +106,8 @@ class UserSettings : public QObject Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) Q_PROPERTY(bool disableCertificateValidation READ disableCertificateValidation WRITE setDisableCertificateValidation NOTIFY disableCertificateValidationChanged) + Q_PROPERTY( + bool useIdenticon READ useIdenticon WRITE setUseIdenticon NOTIFY useIdenticonChanged) UserSettings(); @@ -172,6 +175,7 @@ public: void setHomeserver(QString homeserver); void setDisableCertificateValidation(bool disabled); void setHiddenTags(QStringList hiddenTags); + void setUseIdenticon(bool state); QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; } bool messageHoverHighlight() const { return messageHoverHighlight_; } @@ -230,6 +234,7 @@ public: QString homeserver() const { return homeserver_; } bool disableCertificateValidation() const { return disableCertificateValidation_; } QStringList hiddenTags() const { return hiddenTags_; } + bool useIdenticon() const { return useIdenticon_ && JdenticonProvider::isAvailable(); } signals: void groupViewStateChanged(bool state); @@ -277,6 +282,7 @@ signals: void deviceIdChanged(QString deviceId); void homeserverChanged(QString homeserver); void disableCertificateValidationChanged(bool disabled); + void useIdenticonChanged(bool state); private: // Default to system theme if QT_QPA_PLATFORMTHEME var is set. @@ -330,6 +336,7 @@ private: QString deviceId_; QString homeserver_; QStringList hiddenTags_; + bool useIdenticon_; QSettings settings; @@ -391,6 +398,7 @@ private: Toggle *desktopNotifications_; Toggle *alertOnNotification_; Toggle *avatarCircles_; + Toggle *useIdenticon_; Toggle *useStunServer_; Toggle *decryptSidebar_; Toggle *privacyScreen_; diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 942a4b05..afe53560 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -76,6 +76,8 @@ RoomlistModel::roleNames() const {IsSpace, "isSpace"}, {Tags, "tags"}, {ParentSpaces, "parentSpaces"}, + {IsDirect, "isDirect"}, + {DirectChatOtherUserId, "directChatOtherUserId"}, }; } @@ -129,6 +131,10 @@ RoomlistModel::data(const QModelIndex &index, int role) const list.push_back(QString::fromStdString(t)); return list; } + case Roles::IsDirect: + return room->isDirect(); + case Roles::DirectChatOtherUserId: + return room->directChatOtherUserId(); default: return {}; } @@ -158,6 +164,14 @@ RoomlistModel::data(const QModelIndex &index, int role) const return false; case Roles::Tags: return QStringList(); + case Roles::IsDirect: + // The list of users from the room doesn't contain the invited + // users, so we won't factor the invite into the count + return room.member_count == 1; + case Roles::DirectChatOtherUserId: + return cache::getMembersFromInvite(roomid.toStdString(), 0, 1) + .front() + .user_id; default: return {}; } @@ -190,6 +204,10 @@ RoomlistModel::data(const QModelIndex &index, int role) const return true; case Roles::Tags: return QStringList(); + case Roles::IsDirect: + return false; + case Roles::DirectChatOtherUserId: + return QString{}; // should never be reached default: return {}; } diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h index 6ac6da18..c0a87aee 100644 --- a/src/timeline/RoomlistModel.h +++ b/src/timeline/RoomlistModel.h @@ -65,6 +65,8 @@ public: IsPreviewFetched, Tags, ParentSpaces, + IsDirect, + DirectChatOtherUserId, }; RoomlistModel(TimelineViewManager *parent = nullptr); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 78409e1d..ca303040 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -817,6 +817,11 @@ TimelineModel::syncState(const mtx::responses::State &s) emit roomAvatarUrlChanged(); emit roomNameChanged(); emit roomMemberCountChanged(); + + if (roomMemberCount() <= 2) { + emit isDirectChanged(); + emit directChatOtherUserIdChanged(); + } } else if (std::holds_alternative>(e)) { this->isEncrypted_ = cache::isRoomEncrypted(room_id_.toStdString()); emit encryptionChanged(); @@ -2073,3 +2078,16 @@ TimelineModel::roomMemberCount() const { return (int)cache::client()->memberCount(room_id_.toStdString()); } + +QString +TimelineModel::directChatOtherUserId() const +{ + if (roomMemberCount() < 3) { + QString id; + for (auto member : cache::getMembers(room_id_.toStdString())) + if (member.user_id != UserSettings::instance()->userId()) + id = member.user_id; + return id; + } else + return ""; +} diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 417fbb7f..66e0622e 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -176,6 +176,9 @@ class TimelineModel : public QAbstractListModel Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged) Q_PROPERTY(bool isSpace READ isSpace CONSTANT) Q_PROPERTY(int trustlevel READ trustlevel NOTIFY trustlevelChanged) + Q_PROPERTY(bool isDirect READ isDirect NOTIFY isDirectChanged) + Q_PROPERTY(QString directChatOtherUserId READ directChatOtherUserId NOTIFY + directChatOtherUserIdChanged) Q_PROPERTY(InputBar *input READ input CONSTANT) Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged) @@ -292,6 +295,8 @@ public: bool isEncrypted() const { return isEncrypted_; } crypto::Trust trustlevel() const; int roomMemberCount() const; + bool isDirect() const { return roomMemberCount() <= 2; } + QString directChatOtherUserId() const; std::optional eventById(const QString &id) { @@ -391,6 +396,8 @@ signals: void roomTopicChanged(); void roomAvatarUrlChanged(); void roomMemberCountChanged(); + void isDirectChanged(); + void directChatOtherUserIdChanged(); void permissionsChanged(); void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId); diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 681cbe09..ea231b03 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -141,6 +141,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par , imgProvider(new MxcImageProvider()) , colorImgProvider(new ColorImageProvider()) , blurhashProvider(new BlurhashProvider()) + , jdenticonProvider(new JdenticonProvider()) , callManager_(callManager) , rooms_(new RoomlistModel(this)) , communities_(new CommunitiesModel(this)) @@ -310,6 +311,8 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par view->engine()->addImageProvider("MxcImage", imgProvider); view->engine()->addImageProvider("colorimage", colorImgProvider); view->engine()->addImageProvider("blurhash", blurhashProvider); + if (JdenticonProvider::isAvailable()) + view->engine()->addImageProvider("jdenticon", jdenticonProvider); view->setSource(QUrl("qrc:///qml/Root.qml")); connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette); diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 4dd5e996..8991de55 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -18,6 +18,7 @@ #include "Cache.h" #include "CallManager.h" +#include "JdenticonProvider.h" #include "Logging.h" #include "TimelineModel.h" #include "Utils.h" @@ -141,6 +142,7 @@ private: MxcImageProvider *imgProvider; ColorImageProvider *colorImgProvider; BlurhashProvider *blurhashProvider; + JdenticonProvider *jdenticonProvider; CallManager *callManager_ = nullptr; diff --git a/src/ui/Theme.h b/src/ui/Theme.h index b5bcd4dd..cc39714b 100644 --- a/src/ui/Theme.h +++ b/src/ui/Theme.h @@ -8,12 +8,6 @@ #include namespace ui { -enum class AvatarType -{ - Image, - Letter -}; - // Default font size. const int FontSize = 16;