diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 318446053..90537a20e 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -114,8 +114,8 @@ Kirigami.ApplicationWindow { }).open(); } - function onShowUserDetail(user) { - root.showUserDetail(user); + function onShowUserDetail(user, room) { + root.showUserDetail(user, room); } function goToEvent(event) { @@ -351,9 +351,9 @@ Kirigami.ApplicationWindow { dialog.closeDialog() }) } - function showUserDetail(user) { + function showUserDetail(user, room) { Qt.createComponent("org.kde.neochat", "UserDetailDialog").createObject(root.QQC2.ApplicationWindow.window, { - room: RoomManager.currentRoom ? RoomManager.currentRoom : null, + room: room, user: user, connection: root.connection }).open(); diff --git a/src/qml/UserDetailDialog.qml b/src/qml/UserDetailDialog.qml index 0cb057f20..7b60ccf8f 100644 --- a/src/qml/UserDetailDialog.qml +++ b/src/qml/UserDetailDialog.qml @@ -19,21 +19,8 @@ Kirigami.Dialog { // This dialog is sometimes used outside the context of a room, e.g., when scanning a user's QR code. // Make sure that code is prepared to deal with this property being null property NeoChatRoom room - - /** - * @brief The user's profile object. - * - * Required to interact with the profile and perform action like ignoring. - */ property var user - /** - * @brief The RoomMember object for the user in the current room. - * - * Required to visualise the user. - */ - property var member: root.room.member(user.id) - property NeoChatConnection connection leftPadding: 0 @@ -61,9 +48,9 @@ Kirigami.Dialog { Layout.preferredWidth: Kirigami.Units.iconSizes.huge Layout.preferredHeight: Kirigami.Units.iconSizes.huge - name: root.member.displayName - source: root.member.avatarUrl - color: root.member.color + name: root.room ? root.room.member(root.user.id).displayName : root.user.displayName + source: root.room ? root.room.member(root.user.id).avatarUrl : root.user.avatarUrl + color: root.room ? root.room.member(root.user.id).color : QmlUtils.getUserColor(root.user.hueF) } ColumnLayout { @@ -76,13 +63,13 @@ Kirigami.Dialog { elide: Text.ElideRight wrapMode: Text.NoWrap - text: root.user.displayName + text: root.room ? root.room.member(root.user.id).displayName : root.user.displayName textFormat: Text.PlainText } Kirigami.SelectableLabel { textFormat: TextEdit.PlainText - text: root.member.id + text: root.user.id } } QQC2.AbstractButton { @@ -91,16 +78,16 @@ Kirigami.Dialog { contentItem: Barcode { id: barcode barcodeType: Barcode.QRCode - content: "https://matrix.to/#/" + root.member.id + content: "https://matrix.to/#/" + root.user.id } onClicked: { let map = qrMaximizeComponent.createObject(parent, { text: barcode.content, - title: root.member.displayName, - subtitle: root.member.id, - avatarColor: root.member.color, - avatarSource: root.member.avatarUrl, + 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 }); root.close(); map.open(); @@ -117,46 +104,46 @@ Kirigami.Dialog { } FormCard.FormButtonDelegate { - visible: !root.member.isLocalMember + visible: root.user.id !== root.connection.localUserId && !!root.user action: Kirigami.Action { - text: !!root.user && root.connection.isIgnored(root.user) ? i18n("Unignore this user") : i18n("Ignore this user") + text: !!root.user && root.connection.isIgnored(root.user.id) ? i18n("Unignore this user") : i18n("Ignore this user") icon.name: "im-invisible-user" onTriggered: { root.close(); - root.connection.isIgnored(root.user) ? root.connection.removeFromIgnoredUsers(root.user) : root.connection.addToIgnoredUsers(root.user); + root.connection.isIgnored(root.user.id) ? root.connection.removeFromIgnoredUsers(root.user.id) : root.connection.addToIgnoredUsers(root.user.id); } } } FormCard.FormButtonDelegate { - visible: root.room && !root.member.isLocalMember && room.canSendState("kick") && room.containsUser(root.member.id) && room.getUserPowerLevel(root.member.id) < room.getUserPowerLevel(root.room.localMember.id) + visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("kick") && room.containsUser(root.user.id) && room.getUserPowerLevel(root.user.id) < room.getUserPowerLevel(root.connection.localUserId) action: Kirigami.Action { text: i18n("Kick this user") icon.name: "im-kick-user" onTriggered: { - root.room.kickMember(root.member.id); + root.room.kickMember(root.user.id); root.close(); } } } FormCard.FormButtonDelegate { - visible: root.room && !root.member.isLocalMember && room.canSendState("invite") && !room.containsUser(root.member.id) + visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("invite") && !room.containsUser(root.user.id) action: Kirigami.Action { - enabled: root.room && !root.room.isUserBanned(root.member.id) + enabled: root.room && !root.room.isUserBanned(root.user.id) text: i18n("Invite this user") icon.name: "list-add-user" onTriggered: { - root.room.inviteToRoom(root.member.id); + root.room.inviteToRoom(root.user.id); root.close(); } } } FormCard.FormButtonDelegate { - visible: root.room && !root.member.isLocalMember && room.canSendState("ban") && !room.isUserBanned(root.member.id) && room.getUserPowerLevel(root.member.id) < room.getUserPowerLevel(root.room.localMember.id) + visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("ban") && !room.isUserBanned(root.user.id) && room.getUserPowerLevel(root.user.id) < room.getUserPowerLevel(root.connection.localUserId) action: Kirigami.Action { text: i18n("Ban this user") @@ -165,7 +152,7 @@ Kirigami.Dialog { onTriggered: { (root.QQC2.ApplicationWindow.window as Kirigami.ApplicationWindow).pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'BanSheet'), { room: root.room, - userId: root.member.id + userId: root.user.id }, { title: i18nc("@title", "Ban User"), width: Kirigami.Units.gridUnit * 25 @@ -176,14 +163,14 @@ Kirigami.Dialog { } FormCard.FormButtonDelegate { - visible: root.room && !root.member.isLocalMember && room.canSendState("ban") && room.isUserBanned(root.member.id) + visible: root.room && root.user.id !== root.connection.localUserId && room.canSendState("ban") && room.isUserBanned(root.user.id) action: Kirigami.Action { text: i18n("Unban this user") icon.name: "im-irc" icon.color: Kirigami.Theme.negativeTextColor onTriggered: { - root.room.unban(root.member.id); + root.room.unban(root.user.id); root.close(); } } @@ -197,8 +184,8 @@ Kirigami.Dialog { onTriggered: { let dialog = powerLevelDialog.createObject(this, { room: root.room, - userId: root.member.id, - powerLevel: root.room.getUserPowerLevel(root.member.id) + userId: root.user.id, + powerLevel: root.room.getUserPowerLevel(root.user.id) }); dialog.open(); root.close(); @@ -214,7 +201,7 @@ Kirigami.Dialog { } FormCard.FormButtonDelegate { - visible: root.room && (root.member.isLocalUser || room.canSendState("redact")) + visible: root.room && (root.user.id === root.connection.localUserId || room.canSendState("redact")) action: Kirigami.Action { text: i18n("Remove recent messages by this user") @@ -223,7 +210,7 @@ Kirigami.Dialog { onTriggered: { applicationWindow().pageStack.pushDialogLayer(Qt.createComponent('org.kde.neochat', 'RemoveSheet'), { room: root.room, - userId: root.member.id + userId: root.user.id }, { title: i18nc("@title", "Remove Messages"), width: Kirigami.Units.gridUnit * 25 @@ -234,12 +221,12 @@ Kirigami.Dialog { } FormCard.FormButtonDelegate { - visible: !root.member.isLocalMember + visible: root.user.id !== root.connection.localUserId action: Kirigami.Action { - text: root.connection.directChatExists(root.user.object) ? i18nc("%1 is the name of the user.", "Chat with %1", root.user.escapedDisplayName) : i18n("Invite to private chat") + 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" onTriggered: { - root.room.connection.openOrCreateDirectChat(root.user); + root.connection.openOrCreateDirectChat(root.user.id); root.close(); } } @@ -250,7 +237,7 @@ Kirigami.Dialog { text: i18n("Copy link") icon.name: "username-copy" onTriggered: { - Clipboard.saveText("https://matrix.to/#/" + root.member.id); + Clipboard.saveText("https://matrix.to/#/" + root.user.id); } } } diff --git a/src/roommanager.cpp b/src/roommanager.cpp index adea379f5..4428c1974 100644 --- a/src/roommanager.cpp +++ b/src/roommanager.cpp @@ -146,8 +146,10 @@ void RoomManager::resolveResource(const QString &idOrUri, const QString &action) if ((uri.type() == Uri::RoomAlias || uri.type() == Uri::RoomId) && action != "no_join"_ls) { Q_EMIT askJoinRoom(uri.primaryId()); } - } else { // Invalid cases should have been eliminated earlier - Q_ASSERT(result == Quotient::UriResolved); + } else { + if (result != Quotient::UriResolved) { + return; + } if (uri.type() == Uri::RoomAlias || uri.type() == Uri::RoomId) { connect( @@ -255,9 +257,9 @@ void RoomManager::openRoomForActiveConnection() UriResolveResult RoomManager::visitUser(User *user, const QString &action) { - if (action == "mention"_ls || action.isEmpty()) { + if (action == "mention"_ls || action == "qr"_ls || action.isEmpty()) { user->load(); - Q_EMIT showUserDetail(user); + Q_EMIT showUserDetail(user, action == "qr"_ls ? nullptr : currentRoom()); } else if (action == "_interactive"_ls) { user->requestDirectChat(); } else if (action == "chat"_ls) { diff --git a/src/roommanager.h b/src/roommanager.h index ee69ea33d..74a4004a2 100644 --- a/src/roommanager.h +++ b/src/roommanager.h @@ -261,7 +261,7 @@ Q_SIGNALS: * Ask current room to open the user's details for the give user. * This assumes the user is loaded. */ - void showUserDetail(const Quotient::User *user); + void showUserDetail(const Quotient::User *user, const NeoChatRoom *room); /** * @brief Request a media item is shown maximized. diff --git a/src/utils.cpp b/src/utils.cpp index 9ad03aedb..04d775e8e 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -7,9 +7,23 @@ #include +using namespace Quotient; + bool QmlUtils::isValidJson(const QByteArray &json) { return !QJsonDocument::fromJson(json).isNull(); } +QString QmlUtils::escapeString(const QString &string) +{ + return string.toHtmlEscaped(); +} + +QColor QmlUtils::getUserColor(qreal hueF) +{ + const auto lightness = static_cast(QGuiApplication::instance())->palette().color(QPalette::Active, QPalette::Window).lightnessF(); + // https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal + return QColor::fromHslF(hueF, 1, -0.7 * lightness + 0.9, 1); +} + #include "moc_utils.cpp" diff --git a/src/utils.h b/src/utils.h index 87eb52704..a0d688c26 100644 --- a/src/utils.h +++ b/src/utils.h @@ -3,9 +3,14 @@ #pragma once +#include +#include +#include #include #include +#include + class QmlUtils : public QObject { Q_OBJECT @@ -26,11 +31,30 @@ public: } Q_INVOKABLE bool isValidJson(const QByteArray &json); + Q_INVOKABLE QString escapeString(const QString &string); + Q_INVOKABLE QColor getUserColor(qreal hueF); private: QmlUtils() = default; }; +namespace Utils +{ + +/** + * @brief Get a color for a user from a hueF value. + * + * The lightness of the color will be modified depending on the current palette in + * order to maintain contrast. + */ +inline QColor getUserColor(qreal hueF) +{ + const auto lightness = static_cast(QGuiApplication::instance())->palette().color(QPalette::Active, QPalette::Window).lightnessF(); + // https://github.com/quotient-im/libQuotient/wiki/User-color-coding-standard-draft-proposal + return QColor::fromHslF(hueF, 1, -0.7 * lightness + 0.9, 1); +} +} + namespace TextRegex { static const QRegularExpression endTagType{QStringLiteral("(>| )")};