From 9e7cd0eb09453673aed81536ee1e7d50b0d2d398 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 21 Dec 2025 13:56:26 -0500 Subject: [PATCH] Overhaul user profile UI Our previous iteration is hitting some limitations as the power of profiles on Matrix grows. For example, where do we put common rooms or extra profile fields? I re-arranged everything to group similar actions together - instead of throwing it all into one big list. We basically trade vertical for horizontal space, and gives us more headroom for extra fields when we want to add more. --- src/app/CMakeLists.txt | 2 + src/app/models/commonroomsmodel.cpp | 28 +- src/app/models/commonroomsmodel.h | 6 +- src/app/models/limitermodel.cpp | 41 +++ src/app/models/limitermodel.h | 41 +++ src/app/qml/UserDetailDialog.qml | 495 ++++++++++++++++------------ 6 files changed, 392 insertions(+), 221 deletions(-) create mode 100644 src/app/models/limitermodel.cpp create mode 100644 src/app/models/limitermodel.h diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 6a8384230..e0fa5f494 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -35,6 +35,8 @@ qt_add_library(neochat STATIC models/commonroomsmodel.h texttospeechhelper.h texttospeechhelper.cpp + models/limitermodel.cpp + models/limitermodel.h ) set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES diff --git a/src/app/models/commonroomsmodel.cpp b/src/app/models/commonroomsmodel.cpp index c355f30d4..1fe05853d 100644 --- a/src/app/models/commonroomsmodel.cpp +++ b/src/app/models/commonroomsmodel.cpp @@ -5,6 +5,7 @@ #include "jobs/neochatgetcommonroomsjob.h" #include +#include using namespace Quotient; @@ -39,8 +40,22 @@ void CommonRoomsModel::setUserId(const QString &userId) QVariant CommonRoomsModel::data(const QModelIndex &index, int roleName) const { - Q_UNUSED(index) - Q_UNUSED(roleName) + auto roomId = m_commonRooms[index.row()]; + auto room = connection()->room(roomId); + if (!room) { + return {}; + } + + switch (roleName) { + case Qt::DisplayRole: + case RoomNameRole: + return room->displayName(); + case RoomAvatarRole: + return room->avatarUrl(); + case RoomIdRole: + return roomId; + } + return {}; } @@ -50,6 +65,15 @@ int CommonRoomsModel::rowCount(const QModelIndex &parent) const return m_commonRooms.size(); } +QHash CommonRoomsModel::roleNames() const +{ + return { + {RoomIdRole, "roomId"}, + {RoomNameRole, "roomName"}, + {RoomAvatarRole, "roomAvatar"}, + }; +} + void CommonRoomsModel::reload() { if (!m_connection || m_userId.isEmpty()) { diff --git a/src/app/models/commonroomsmodel.h b/src/app/models/commonroomsmodel.h index f0c8cc068..b6afc060a 100644 --- a/src/app/models/commonroomsmodel.h +++ b/src/app/models/commonroomsmodel.h @@ -24,7 +24,9 @@ class CommonRoomsModel : public QAbstractListModel public: enum Roles { - RoomIdRole = Qt::DisplayRole, + RoomIdRole = Qt::UserRole, + RoomNameRole, + RoomAvatarRole, }; Q_ENUM(Roles) @@ -39,6 +41,8 @@ public: [[nodiscard]] QVariant data(const QModelIndex &index, int roleName) const override; [[nodiscard]] Q_INVOKABLE int rowCount(const QModelIndex &parent = {}) const override; + QHash roleNames() const override; + Q_SIGNALS: void connectionChanged(); void userIdChanged(); diff --git a/src/app/models/limitermodel.cpp b/src/app/models/limitermodel.cpp new file mode 100644 index 000000000..c5834eef7 --- /dev/null +++ b/src/app/models/limitermodel.cpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "models/limitermodel.h" + +LimiterModel::LimiterModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ + connect(this, &QSortFilterProxyModel::rowsInserted, this, &LimiterModel::extraCountChanged); + connect(this, &QSortFilterProxyModel::rowsRemoved, this, &LimiterModel::extraCountChanged); + connect(this, &QSortFilterProxyModel::modelReset, this, &LimiterModel::extraCountChanged); +} + +int LimiterModel::maximumCount() const +{ + return m_maximumCount; +} + +void LimiterModel::setMaximumCount(int maximumCount) +{ + if (m_maximumCount != maximumCount) { + m_maximumCount = maximumCount; + Q_EMIT maximumCountChanged(); + } +} + +int LimiterModel::extraCount() const +{ + if (sourceModel()) { + return std::max(sourceModel()->rowCount() - maximumCount(), 0); + } + return 0; +} + +bool LimiterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + Q_UNUSED(source_parent) + return source_row < maximumCount(); +} + +#include "moc_limitermodel.cpp" diff --git a/src/app/models/limitermodel.h b/src/app/models/limitermodel.h new file mode 100644 index 000000000..b05db805f --- /dev/null +++ b/src/app/models/limitermodel.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Joshua Goins +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include + +/** + * @class LimiterModel + * + * @brief Takes a source QAbstractItemModel model and only displays a desired maximum amount. + * + * Also gives you the remaining (filtered out) items, useful for sticking in a label or somesuch. + */ +class LimiterModel : public QSortFilterProxyModel +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(int maximumCount READ maximumCount WRITE setMaximumCount NOTIFY maximumCountChanged) + Q_PROPERTY(int extraCount READ extraCount NOTIFY extraCountChanged) + +public: + explicit LimiterModel(QObject *parent = nullptr); + + [[nodiscard]] int maximumCount() const; + void setMaximumCount(int maximumCount); + + [[nodiscard]] int extraCount() const; + +Q_SIGNALS: + void maximumCountChanged(); + void extraCountChanged(); + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + +private: + int m_maximumCount = 0; +}; diff --git a/src/app/qml/UserDetailDialog.qml b/src/app/qml/UserDetailDialog.qml index 8c2b9e852..7ab139c0d 100644 --- a/src/app/qml/UserDetailDialog.qml +++ b/src/app/qml/UserDetailDialog.qml @@ -23,27 +23,43 @@ Kirigami.Dialog { property NeoChatConnection connection - leftPadding: 0 - rightPadding: 0 - topPadding: 0 - bottomPadding: 0 + property CommonRoomsModel model: CommonRoomsModel { + connection: root.connection + userId: root.user.id + } + + property LimiterModel limiterModel: LimiterModel { + maximumCount: 5 + sourceModel: root.model + } + + readonly property bool isSelf: root.user.id === root.connection.localUserId + readonly property bool hasMutualRooms: root.model.count > 0 + readonly property bool isRoomProfile: root.room + readonly property string shareUrl: "https://matrix.to/#/" + root.user.id + + leftPadding: Kirigami.Units.largeSpacing * 2 + rightPadding: Kirigami.Units.largeSpacing * 2 + topPadding: Kirigami.Units.largeSpacing * 2 + bottomPadding: Kirigami.Units.largeSpacing * 2 standardButtons: Kirigami.Dialog.NoButton width: Math.min(QQC2.ApplicationWindow.window.width, Kirigami.Units.gridUnit * 24) title: i18nc("@title:menu Account details dialog", "Account Details") + header: null + contentItem: ColumnLayout { spacing: 0 + RowLayout { id: detailRow - Layout.fillWidth: true - Layout.leftMargin: Kirigami.Units.largeSpacing - Layout.rightMargin: Kirigami.Units.largeSpacing - Layout.topMargin: Kirigami.Units.largeSpacing - Layout.bottomMargin: Kirigami.Units.largeSpacing + spacing: Kirigami.Units.largeSpacing + Layout.fillWidth: true + KirigamiComponents.Avatar { id: avatar Layout.preferredWidth: Kirigami.Units.iconSizes.huge @@ -75,242 +91,285 @@ Kirigami.Dialog { id: idLabel textFormat: TextEdit.PlainText text: idLabelTextMetrics.elidedText + color: Kirigami.Theme.disabledTextColor TextMetrics { id: idLabelTextMetrics text: root.user.id elide: Qt.ElideRight - elideWidth: root.availableWidth - avatar.width - qrButton.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin + elideWidth: root.availableWidth - avatar.width - detailRow.spacing * 2 - detailRow.Layout.leftMargin - detailRow.Layout.rightMargin } } - QQC2.Label { - property CommonRoomsModel model: CommonRoomsModel { - connection: root.connection - userId: root.user.id - } - - text: i18ncp("@info", "One mutual room", "%1 mutual rooms", model.count) - color: Kirigami.Theme.disabledTextColor - visible: model.count > 0 - + Kirigami.ActionToolBar { Layout.topMargin: Kirigami.Units.smallSpacing + + actions: [ + Kirigami.Action { + text: i18nc("@action:intoolbar Message this user directly", "Message") + icon.name: "document-send-symbolic" + + onTriggered: { + root.close(); + root.connection.requestDirectChat(root.user.id); + } + }, + Kirigami.Action { + icon.name: "im-invisible-user-symbolic" + text: root.connection.isIgnored(root.user.id) ? i18nc("@action:intoolbar Unignore or 'unblock' this user", "Unignore") : i18nc("@action:intoolbar Ignore or 'block' this user", "Ignore") + + onTriggered: { + root.close(); + root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id); + } + }, + Kirigami.Action { + text: i18nc("@action:intoolbar Copy shareable link for this user", "Copy Link") + icon.name: "username-copy-symbolic" + + onTriggered: Clipboard.saveText(root.shareUrl) + }, + Kirigami.Action { + text: i18nc("@action:intoolbar Search for this user's messages.", "Search Messages…") + icon.name: "search-symbolic" + + onTriggered: { + ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), { + room: root.room, + senderId: root.user.id + }, { + title: i18nc("@action:title", "Search") + }); + root.close(); + } + }, + Kirigami.Action { + text: i18nc("@action:intoolbar", "Show QR Code") + icon.name: "view-barcode-qr-symbolic" + + onTriggered: { + let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, { + text: root.shareUrl, + title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName, + subtitle: root.user.id, + avatarColor: root.room?.member(root.user.id).color, + avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl + }) as QrCodeMaximizeComponent; + root.close(); + qrCode.open(); + } + }, + Kirigami.Action { + text: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report…") + icon.name: "dialog-warning-symbolic" + visible: root.connection.supportsMatrixSpecVersion("v1.13") + + onTriggered: { + let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), { + title: i18nc("@title:dialog", "Report User"), + placeholder: i18nc("@info:placeholder", "Reason for reporting this user"), + icon: "dialog-warning-symbolic", + actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report") + }, { + title: i18nc("@title", "Report User"), + width: Kirigami.Units.gridUnit * 25 + }) as ReasonDialog; + dialog.accepted.connect(reason => { + root.connection.reportUser(root.user.id, reason); + }); + } + } + ] } } - QQC2.AbstractButton { - id: qrButton - Layout.minimumHeight: avatar.height * 0.75 - Layout.maximumHeight: avatar.height * 1.5 - Layout.maximumWidth: avatar.height * 1.5 + } - contentItem: Barcode { - id: barcode - barcodeType: Barcode.QRCode - content: "https://matrix.to/#/" + root.user.id - } + Kirigami.Heading { + text: i18nc("@title Moderation actions for this user", "Moderation") + level: 2 + visible: root.isRoomProfile && moderationToolbar.actions.filter(function (it) { return it.visible; }).length > 0 - onClicked: { - let qrCode = Qt.createComponent('org.kde.neochat', 'QrCodeMaximizeComponent').createObject(QQC2.Overlay.overlay, { - text: barcode.content, - title: root.room ? root.room.member(root.user.id).displayName : root.user.displayName, - subtitle: root.user.id, - avatarColor: root.room?.member(root.user.id).color, - avatarSource: root.room? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl - }) as QrCodeMaximizeComponent; - root.close(); - qrCode.open(); + Layout.topMargin: Kirigami.Units.largeSpacing + } + + Kirigami.ActionToolBar { + id: moderationToolbar + + flat: false + visible: root.isRoomProfile + + Layout.fillWidth: true + Layout.topMargin: Kirigami.Units.smallSpacing + + actions: [ + Kirigami.Action { + visible: !root.isSelf && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId) + + text: i18nc("@action:button Kick the user from the room", "Kick…") + icon.name: "im-kick-user" + onTriggered: { + let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), { + title: i18nc("@title:dialog", "Kick User"), + placeholder: i18nc("@info:placeholder", "Reason for kicking this user"), + actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"), + icon: "im-kick-user" + }, { + title: i18nc("@title:dialog", "Kick User"), + width: Kirigami.Units.gridUnit * 25 + }); + dialog.accepted.connect(reason => { + root.room.kickMember(root.user.id, reason); + }); + root.close(); + } + }, + Kirigami.Action { + visible: !root.isSelf && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId) + + text: i18nc("@action:button Ban this user from the room", "Ban…") + icon.name: "im-ban-user" + icon.color: Kirigami.Theme.negativeTextColor + onTriggered: { + let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), { + title: i18nc("@title:dialog", "Ban User"), + placeholder: i18nc("@info:placeholder", "Reason for banning this user"), + actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"), + icon: "im-ban-user" + }, { + title: i18nc("@title:dialog", "Ban User"), + width: Kirigami.Units.gridUnit * 25 + }); + dialog.accepted.connect(reason => { + root.room.ban(root.user.id, reason); + }); + root.close(); + } + }, + Kirigami.Action { + visible: !root.isSelf && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id) + + text: i18nc("@action:button Unban the user from this room", "Unban") + icon.name: "im-irc" + icon.color: Kirigami.Theme.negativeTextColor + onTriggered: { + root.room.unban(root.user.id); + root.close(); + } + }, + Kirigami.Action { + visible: (root.user.id === root.connection.localUserId || root.room.canSendState("redact")) + + text: i18nc("@action:button Remove messages from the user in this room", "Remove Messages…") + icon.name: "delete" + icon.color: Kirigami.Theme.negativeTextColor + onTriggered: { + let dialog = ((root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), { + title: i18nc("@title:dialog", "Remove Messages"), + placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"), + actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"), + icon: "delete" + }, { + title: i18nc("@title", "Remove Messages"), + width: Kirigami.Units.gridUnit * 25 + }); + dialog.accepted.connect(reason => { + root.room.deleteMessagesByUser(root.user.id, reason); + }); + root.close(); + } } + ] + } + + Kirigami.Heading { + text: i18nc("@title Role such as 'Admin' or 'Moderator' for this user", "Role") + level: 2 + visible: root.isRoomProfile + + Layout.topMargin: Kirigami.Units.largeSpacing + } + + RowLayout { + spacing: Kirigami.Units.smallSpacing + visible: root.isRoomProfile + + Layout.topMargin: Kirigami.Units.smallSpacing + + QQC2.Label { + text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : "" + } + + QQC2.Button { + visible: root.room.canSendState("m.room.power_levels") + text: i18nc("@action:button Set the power level (such as 'Admin') for this user", "Set Power Level") + icon.name: "document-edit-symbolic" + display: QQC2.AbstractButton.IconOnly QQC2.ToolTip.visible: hovered - QQC2.ToolTip.text: barcode.content + QQC2.ToolTip.text: text QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } - } - Kirigami.Chip { - visible: root.room - text: root.room ? QmlUtils.nameForPowerLevelValue(root.room.memberEffectivePowerLevel(root.user.id)) : "" - closable: false - checkable: false + onClicked: { + (powerLevelDialog.createObject(this, { + room: root.room, + userId: root.user.id, + powerLevel: root.room.memberEffectivePowerLevel(root.user.id) + }) as PowerLevelDialog).open(); + root.close(); + } - Layout.leftMargin: Kirigami.Units.largeSpacing - Layout.bottomMargin: Kirigami.Units.largeSpacing - } - - Kirigami.Separator { - Layout.fillWidth: true - } - - FormCard.FormButtonDelegate { - visible: root.user.id !== root.connection.localUserId && !!root.user - text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user") - icon.name: "im-invisible-user" - onClicked: { - root.close(); - root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id); - } - } - - FormCard.FormButtonDelegate { - visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("kick") && root.room.containsUser(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId) - - text: i18nc("@action:button", "Kick this user") - icon.name: "im-kick-user" - onClicked: { - let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), { - title: i18nc("@title:dialog", "Kick User"), - placeholder: i18nc("@info:placeholder", "Reason for kicking this user"), - actionText: i18nc("@action:button 'Kick' as in 'Kick this user from the room'", "Kick"), - icon: "im-kick-user" - }, { - title: i18nc("@title:dialog", "Kick User"), - width: Kirigami.Units.gridUnit * 25 - }); - dialog.accepted.connect(reason => { - root.room.kickMember(root.user.id, reason); - }); - root.close(); - } - } - - FormCard.FormButtonDelegate { - visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("invite") && !root.room.containsUser(root.user.id) - - enabled: root.room && !root.room.isUserBanned(root.user.id) - text: i18nc("@action:button", "Invite this user") - icon.name: "list-add-user" - onClicked: { - root.room.inviteToRoom(root.user.id); - root.close(); - } - } - - FormCard.FormButtonDelegate { - visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("ban") && !root.room.isUserBanned(root.user.id) && root.room.memberEffectivePowerLevel(root.user.id) < root.room.memberEffectivePowerLevel(root.connection.localUserId) - - text: i18nc("@action:button", "Ban this user") - icon.name: "im-ban-user" - icon.color: Kirigami.Theme.negativeTextColor - onClicked: { - let dialog = (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), { - title: i18nc("@title:dialog", "Ban User"), - placeholder: i18nc("@info:placeholder", "Reason for banning this user"), - actionText: i18nc("@action:button 'Ban' as in 'Ban this user'", "Ban"), - icon: "im-ban-user" - }, { - title: i18nc("@title:dialog", "Ban User"), - width: Kirigami.Units.gridUnit * 25 - }); - dialog.accepted.connect(reason => { - root.room.ban(root.user.id, reason); - }); - root.close(); - } - } - - FormCard.FormButtonDelegate { - visible: root.room && root.user.id !== root.connection.localUserId && root.room.canSendState("ban") && root.room.isUserBanned(root.user.id) - - text: i18nc("@action:button", "Unban this user") - icon.name: "im-irc" - icon.color: Kirigami.Theme.negativeTextColor - onClicked: { - root.room.unban(root.user.id); - root.close(); - } - } - - FormCard.FormButtonDelegate { - visible: root.room && root.room.canSendState("m.room.power_levels") - text: i18nc("@action:button", "Set user power level") - icon.name: "visibility" - onClicked: { - (powerLevelDialog.createObject(this, { - room: root.room, - userId: root.user.id, - powerLevel: root.room.memberEffectivePowerLevel(root.user.id) - }) as PowerLevelDialog).open(); - root.close(); - } - - Component { - id: powerLevelDialog - PowerLevelDialog { + Component { id: powerLevelDialog + PowerLevelDialog { + id: powerLevelDialog + } } } } - FormCard.FormButtonDelegate { - visible: root.room && (root.user.id === root.connection.localUserId || root.room.canSendState("redact")) + Kirigami.Heading { + text: i18nc("@title The set of common rooms between your current user and the one shown", "Mutual Rooms") + level: 4 + visible: !root.isSelf && root.hasMutualRooms - text: i18nc("@action:button", "Remove recent messages by this user") - icon.name: "delete" - icon.color: Kirigami.Theme.negativeTextColor - onClicked: { - let dialog = ((QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), { - title: i18nc("@title:dialog", "Remove Messages"), - placeholder: i18nc("@info:placeholder", "Reason for removing this user's recent messages"), - actionText: i18nc("@action:button 'Remove' as in 'Remove these messages'", "Remove"), - icon: "delete" - }, { - title: i18nc("@title", "Remove Messages"), - width: Kirigami.Units.gridUnit * 25 - }); - dialog.accepted.connect(reason => { - root.room.deleteMessagesByUser(root.user.id, reason); - }); - root.close(); + Layout.topMargin: Kirigami.Units.largeSpacing + } + + RowLayout { + spacing: Kirigami.Units.smallSpacing + visible: !root.isSelf && root.hasMutualRooms + + Layout.topMargin: Kirigami.Units.smallSpacing + + Repeater { + model: root.limiterModel + + delegate: KirigamiComponents.AvatarButton { + required property string roomName + required property string roomAvatar + required property string roomId + + Layout.preferredWidth: Kirigami.Units.iconSizes.medium + Layout.preferredHeight: Kirigami.Units.iconSizes.medium + + name: roomName + source: roomAvatar + + onClicked: { + root.close(); + RoomManager.resolveResource(roomId); + } + + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.text: name + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } } - } - FormCard.FormButtonDelegate { - visible: root.user.id !== root.connection.localUserId - text: root.connection.directChatExists(root.user) ? i18nc("%1 is the name of the user.", "Chat with %1", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) : i18n("Invite to private chat") - icon.name: "document-send" - onClicked: { - root.connection.requestDirectChat(root.user.id); - root.close(); - } - } - - FormCard.FormButtonDelegate { - text: i18nc("@action:button %1 is the name of the user.", "Search room for %1's messages", root.room ? root.room.member(root.user.id).htmlSafeDisplayName : QmlUtils.escapeString(root.user.displayName)) - icon.name: "search-symbolic" - onClicked: { - ((QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RoomSearchPage'), { - room: root.room, - senderId: root.user.id - }, { - title: i18nc("@action:title", "Search") - }); - root.close(); - } - } - - FormCard.FormButtonDelegate { - text: i18n("Copy link") - icon.name: "username-copy" - onClicked: Clipboard.saveText("https://matrix.to/#/" + root.user.id) - } - - FormCard.FormButtonDelegate { - text: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report…") - icon.name: "dialog-warning-symbolic" - visible: root.connection.supportsMatrixSpecVersion("v1.13") - onClicked: { - let dialog = ((QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack as Kirigami.PageRow).pushDialogLayer(Qt.createComponent('org.kde.neochat', 'ReasonDialog'), { - title: i18nc("@title:dialog", "Report User"), - placeholder: i18nc("@info:placeholder", "Reason for reporting this user"), - icon: "dialog-warning-symbolic", - actionText: i18nc("@action:button 'Report' as in 'Report this user to the administrators'", "Report") - }, { - title: i18nc("@title", "Report User"), - width: Kirigami.Units.gridUnit * 25 - }) as ReasonDialog; - dialog.accepted.connect(reason => { - root.connection.reportUser(root.user.id, reason); - }); + QQC2.Label { + text: i18ncp("@info:label And '%1' more rooms you have in common with this user, but are not shown", "and 1 more…", "and %1 more…", root.limiterModel.extraCount) + visible: root.limiterModel.extraCount > 0 + color: Kirigami.Theme.disabledTextColor } } }