From 9596d618e08519de01b1bc895f29d54f24f0cccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Wed, 27 Nov 2024 16:33:25 +0100 Subject: [PATCH] feat: Improvements for community admins - add ability to copy display name of a message author - add possibility to search for a member by a un/compressed chat key - adjust the search field placeholder text to "Search by member name or chat key" - update SB with some more variations - cleanup some the signal handling Fixes #16790 --- storybook/pages/StatusMessagePage.qml | 15 ++++- .../StatusContactVerificationIcons.qml | 2 +- .../Components/StatusMemberListItem.qml | 15 ++--- .../src/StatusQ/Components/StatusMessage.qml | 5 +- .../Components/StatusMessageHeader.qml | 41 +++++++----- .../Components/StatusMessageSenderDetails.qml | 3 +- .../statusMessage/StatusMessageReply.qml | 6 +- .../panels/MembersSettingsPanel.qml | 25 -------- .../Communities/panels/MembersTabPanel.qml | 63 +++++++++++-------- .../views/CommunitySettingsView.qml | 4 +- ui/imports/shared/views/chat/MessageView.qml | 21 ++----- 11 files changed, 97 insertions(+), 103 deletions(-) diff --git a/storybook/pages/StatusMessagePage.qml b/storybook/pages/StatusMessagePage.qml index ddf8e452341..56976bec939 100644 --- a/storybook/pages/StatusMessagePage.qml +++ b/storybook/pages/StatusMessagePage.qml @@ -66,6 +66,17 @@ SplitView { trustIndicator: StatusContactVerificationIcons.TrustedType.None outgoingStatus: StatusMessage.OutgoingStatus.Delivered } + ListElement { + timestamp: 1667937930159 + senderId: "zqdeadbeef86" + senderDisplayName: "8⃣6⃣.stateofus.eth" + contentType: StatusMessage.ContentType.Text + message: "Test message for a user with emoji + ENS name" + isContact: false + isAReply: false + trustIndicator: StatusContactVerificationIcons.TrustedType.None + outgoingStatus: StatusMessage.OutgoingStatus.Delivered + } ListElement { timestamp: 1719769718000 senderId: "zq123456790" @@ -186,6 +197,7 @@ SplitView { SplitView.fillWidth: true SplitView.fillHeight: true color: Theme.palette.statusAppLayout.rightPanelBackgroundColor + clip: true ListView { anchors.margins: 16 @@ -236,7 +248,7 @@ SplitView { onReplyProfileClicked: logs.logEvent("StatusMessage::replyProfileClicked") onReplyMessageClicked: logs.logEvent("StatusMessage::replyMessageClicked") onResendClicked: logs.logEvent("StatusMessage::resendClicked") - onLinkActivated: logs.logEvent("StatusMessage::linkActivated" + link) + onLinkActivated: logs.logEvent("StatusMessage::linkActivated", ["link"], arguments) onImageClicked: logs.logEvent("StatusMessage::imageClicked") } } @@ -254,3 +266,4 @@ SplitView { } // category: Components +// status: good diff --git a/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml b/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml index 15ad86fb497..64127d7a1d5 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml @@ -1,4 +1,4 @@ -import QtQuick 2.14 +import QtQuick 2.15 import StatusQ.Core 0.1 import StatusQ.Controls 0.1 diff --git a/ui/StatusQ/src/StatusQ/Components/StatusMemberListItem.qml b/ui/StatusQ/src/StatusQ/Components/StatusMemberListItem.qml index 22171ed42ff..ddc3e4acef2 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusMemberListItem.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusMemberListItem.qml @@ -75,12 +75,13 @@ ItemDelegate { /*! \qmlproperty string StatusMemberListItem::status This property holds the connectivity status of the member represented. - 0 - offline - 1 - online - 2 - doNotDisturb - 3 - idle + + int unknown: -1 + int inactive: 0 + int online: 1 + */ - // FIXME: move Constants.userStatus from status-desktop + // FIXME: move Constants.onlineStatus from status-desktop property int status: 0 /*! \qmlproperty string StatusMemberListItem::isAdmin @@ -228,7 +229,7 @@ ItemDelegate { StatusToolTip { text: parent.text - delay: 0 + delay: 50 visible: parent.truncated && primaryTextHandler.hovered } } @@ -252,7 +253,7 @@ ItemDelegate { StatusToolTip { text: parent.text - delay: 0 + delay: 50 visible: parent.truncated && secondaryTextHandler.hovered } } diff --git a/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml b/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml index 3258579d692..5dea4846fe3 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusMessage.qml @@ -81,9 +81,8 @@ Control { property StatusMessageDetails messageDetails: StatusMessageDetails {} property StatusMessageDetails replyDetails: StatusMessageDetails {} - signal clicked(var sender, var mouse) signal profilePictureClicked(var sender, var mouse) - signal senderNameClicked(var sender, var mouse) + signal senderNameClicked(var sender) signal replyProfileClicked(var sender, var mouse) signal replyMessageClicked(var mouse) @@ -265,7 +264,7 @@ Control { amISender: root.messageDetails.amISender messageOriginInfo: root.messageDetails.messageOriginInfo resendError: root.messageDetails.amISender ? root.resendError : "" - onClicked: root.senderNameClicked(sender, mouse) + onClicked: (sender) => root.senderNameClicked(sender) onResendClicked: root.resendClicked() timestamp: root.timestamp showFullTimestamp: root.isInPinnedPopup diff --git a/ui/StatusQ/src/StatusQ/Components/StatusMessageHeader.qml b/ui/StatusQ/src/StatusQ/Components/StatusMessageHeader.qml index 7c06c412715..8ff918e357c 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusMessageHeader.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusMessageHeader.qml @@ -1,5 +1,5 @@ -import QtQuick 2.14 -import QtQuick.Layouts 1.14 +import QtQuick 2.15 +import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 @@ -28,7 +28,7 @@ Item { property int outgoingStatus: StatusMessage.OutgoingStatus.Unknown property bool showOutgointStatusLabel: false - signal clicked(var sender, var mouse) + signal clicked(var sender) signal resendClicked() implicitHeight: layout.implicitHeight @@ -46,7 +46,7 @@ Item { spacing: 4 width: parent.width - StatusBaseText { + TextEdit { id: primaryDisplayName objectName: "StatusMessageHeader_DisplayName" verticalAlignment: Text.AlignVCenter @@ -54,21 +54,31 @@ Item { Layout.maximumWidth: Math.ceil(implicitWidth) Layout.bottomMargin: 2 // offset for the underline to stay vertically centered font.weight: Font.Medium - font.underline: mouseArea.containsMouse + font.underline: root.displayNameClickable && hhandler.hovered + font.family: Theme.baseFont.name font.pixelSize: Theme.primaryTextFontSize wrapMode: Text.WordWrap color: Theme.palette.primaryColor1 + selectionColor: Theme.palette.primaryColor2 + selectedTextColor: Theme.palette.primaryColor3 text: root.amISender ? qsTr("You") : Emoji.parse(root.sender.displayName) - MouseArea { - id: mouseArea - anchors.fill: parent - cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor - acceptedButtons: Qt.LeftButton | Qt.RightButton + readOnly: true + selectByMouse: true + textFormat: TextEdit.AutoText + + // to make the text easier to select, but w/o inflating the spacing with other items in the parent RowLayout + leftPadding: 4 + rightPadding: 4 + Layout.leftMargin: -4 + Layout.rightMargin: -4 + + HoverHandler { + id: hhandler + cursorShape: !!parent.selectedText ? Qt.IBeamCursor : root.displayNameClickable ? Qt.PointingHandCursor : undefined + } + TapHandler { enabled: root.displayNameClickable - hoverEnabled: true - onClicked: { - root.clicked(this, mouse) - } + onSingleTapped: root.clicked(this) } } @@ -77,7 +87,6 @@ Item { active: root.messageOriginInfo asynchronous: true sourceComponent: StatusBaseText { - id: messageOriginInfo verticalAlignment: Text.AlignVCenter color: Theme.palette.baseColor1 font.pixelSize: Theme.asideTextFontSize @@ -90,7 +99,6 @@ Item { active: !root.amISender asynchronous: true sourceComponent: StatusContactVerificationIcons { - id: verificationIcons isContact: root.isContact trustIndicator: root.trustIndicator } @@ -148,7 +156,6 @@ Item { Component { id: dotComponent StatusBaseText { - id: dot verticalAlignment: Text.AlignVCenter font.pixelSize: Theme.asideTextFontSize color: Theme.palette.baseColor1 diff --git a/ui/StatusQ/src/StatusQ/Components/StatusMessageSenderDetails.qml b/ui/StatusQ/src/StatusQ/Components/StatusMessageSenderDetails.qml index c3361012d0f..2f4a65bbdbb 100644 --- a/ui/StatusQ/src/StatusQ/Components/StatusMessageSenderDetails.qml +++ b/ui/StatusQ/src/StatusQ/Components/StatusMessageSenderDetails.qml @@ -1,4 +1,5 @@ -import QtQuick 2.0 +import QtQuick 2.15 + import StatusQ.Core 0.1 QtObject { diff --git a/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusMessageReply.qml b/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusMessageReply.qml index 99893e86f34..b6aff9a2e16 100644 --- a/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusMessageReply.qml +++ b/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusMessageReply.qml @@ -1,6 +1,6 @@ -import QtQuick 2.14 -import QtQuick.Shapes 1.13 -import QtQuick.Layouts 1.14 +import QtQuick 2.15 +import QtQuick.Shapes 1.15 +import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 diff --git a/ui/app/AppLayouts/Communities/panels/MembersSettingsPanel.qml b/ui/app/AppLayouts/Communities/panels/MembersSettingsPanel.qml index d381494a6ef..061d0612c3d 100644 --- a/ui/app/AppLayouts/Communities/panels/MembersSettingsPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/MembersSettingsPanel.qml @@ -26,7 +26,6 @@ SettingsPage { property int memberRole property bool editable: true - signal membershipRequestsClicked() signal kickUserClicked(string id) signal banUserClicked(string id, bool deleteAllMessages) signal unbanUserClicked(string id) @@ -123,12 +122,6 @@ SettingsPage { rootStore: root.rootStore utilsStore: root.utilsStore memberRole: root.memberRole - placeholderText: { - if (root.membersModel.ModelCount.count === 0) - return qsTr("No members to search") - - return qsTr("Search %1's %n member(s)", "", root.membersModel ? root.membersModel.ModelCount.count : 0).arg(root.communityName) - } panelType: MembersTabPanel.TabType.AllMembers Layout.fillWidth: true @@ -156,12 +149,6 @@ SettingsPage { rootStore: root.rootStore utilsStore: root.utilsStore memberRole: root.memberRole - placeholderText: { - if (root.pendingMembersModel.ModelCount.count === 0) - return qsTr("No pending requests to search") - - return qsTr("Search %1's %n pending request(s)", "", root.pendingMembersModel.ModelCount.count).arg(root.communityName) - } panelType: MembersTabPanel.TabType.PendingRequests Layout.fillWidth: true @@ -176,12 +163,6 @@ SettingsPage { rootStore: root.rootStore utilsStore: root.utilsStore memberRole: root.memberRole - placeholderText: { - if (root.declinedMembersModel.ModelCount.count === 0) - return qsTr("No rejected members to search") - - return qsTr("Search %1's %n rejected member(s)", "", root.declinedMembersModel.ModelCount.count).arg(root.communityName) - } panelType: MembersTabPanel.TabType.DeclinedRequests Layout.fillWidth: true @@ -195,12 +176,6 @@ SettingsPage { rootStore: root.rootStore utilsStore: root.utilsStore memberRole: root.memberRole - placeholderText: { - if (root.bannedMembersModel.ModelCount.count === 0) - return qsTr("No banned members to search") - - return qsTr("Search %1's %n banned member(s)", "", root.bannedMembersModel.ModelCount.count).arg(root.communityName) - } panelType: MembersTabPanel.TabType.BannedMembers Layout.fillWidth: true diff --git a/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml b/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml index cc13e215d68..1aa572c807a 100644 --- a/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml @@ -24,7 +24,7 @@ import SortFilterProxyModel 0.2 Item { id: root - property string placeholderText + property string placeholderText: qsTr("Search by member name or chat key") property var model property RootStore rootStore property SharedStores.UtilsStore utilsStore @@ -60,7 +60,7 @@ Item { Layout.preferredWidth: 400 Layout.leftMargin: 12 placeholderText: root.placeholderText - enabled: !!model && model.count > 0 + enabled: !!root.model && !root.model.ModelCount.empty } StatusListView { @@ -71,8 +71,16 @@ Item { Layout.fillHeight: true model: SortFilterProxyModel { + id: filteredModel sourceModel: root.model + function searchPredicate(ensName, displayName, aliasName) { + const lowerCaseSearchString = memberSearch.text.toLowerCase() + const secondaryName = ProfileUtils.displayName("", ensName, displayName, aliasName) + + return secondaryName.toLowerCase().includes(lowerCaseSearchString) + } + sorters : [ StringSorter { roleName: "preferredDisplayName" @@ -81,21 +89,23 @@ Item { ] filters: AnyOf { + enabled: memberSearch.text !== "" + // substring search for either nickname or the other primary/secondary display name SearchFilter { roleName: "localNickname" searchPhrase: memberSearch.text } - SearchFilter { - roleName: "displayName" - searchPhrase: memberSearch.text - } - SearchFilter { - roleName: "ensName" - searchPhrase: memberSearch.text + FastExpressionFilter { + expression: { + memberSearch.text + return filteredModel.searchPredicate(model.ensName, model.displayName, model.alias) + } + expectedRoles: ["ensName", "displayName", "alias"] } - SearchFilter { - roleName: "alias" - searchPhrase: memberSearch.text + // exact search for the full key + ValueFilter { + roleName: "compressedPubKey" + value: memberSearch.text } } } @@ -346,25 +356,24 @@ Item { ProfileContextMenu { id: memberContextMenuView - property string pubKey + required property string pubKey - onOpenProfileClicked: Global.openProfilePopup(memberContextMenuView.pubKey, null) + onOpenProfileClicked: Global.openProfilePopup(pubKey, null) onCreateOneToOneChat: { Global.changeAppSectionBySectionType(Constants.appSection.chat) - root.rootStore.chatCommunitySectionModule.createOneToOneChat("", membersContextMenuView.pubKey, "") + root.rootStore.chatCommunitySectionModule.createOneToOneChat("", pubKey, "") } - onReviewContactRequest: Global.openReviewContactRequestPopup(memberContextMenuView.pubKey, null) - onSendContactRequest: Global.openContactRequestPopup(memberContextMenuView.pubKey, null) - onEditNickname: Global.openNicknamePopupRequested(memberContextMenuView.pubKey, null) - onRemoveNickname: root.rootStore.contactsStore.changeContactNickname(memberContextMenuView.pubKey, - "", memberContextMenuView.displayName, true) - onUnblockContact: Global.unblockContactRequested(memberContextMenuView.pubKey) - onMarkAsUntrusted: Global.markAsUntrustedRequested(memberContextMenuView.pubKey) - onRemoveTrustStatus: root.rootStore.contactsStore.removeTrustStatus(memberContextMenuView.pubKey) - onRemoveContact: Global.removeContactRequested(memberContextMenuView.pubKey) - onBlockContact: Global.blockContactRequested(memberContextMenuView.pubKey) - onMarkAsTrusted: Global.openMarkAsIDVerifiedPopup(memberContextMenuView.pubKey, null) - onRemoveTrustedMark: Global.openRemoveIDVerificationDialog(memberContextMenuView.pubKey, null) + onReviewContactRequest: Global.openReviewContactRequestPopup(pubKey, null) + onSendContactRequest: Global.openContactRequestPopup(pubKey, null) + onEditNickname: Global.openNicknamePopupRequested(pubKey, null) + onRemoveNickname: root.rootStore.contactsStore.changeContactNickname(pubKey, "", displayName, true) + onUnblockContact: Global.unblockContactRequested(pubKey) + onMarkAsUntrusted: Global.markAsUntrustedRequested(pubKey) + onRemoveTrustStatus: root.rootStore.contactsStore.removeTrustStatus(pubKey) + onRemoveContact: Global.removeContactRequested(pubKey) + onBlockContact: Global.blockContactRequested(pubKey) + onMarkAsTrusted: Global.openMarkAsIDVerifiedPopup(pubKey, null) + onRemoveTrustedMark: Global.openRemoveIDVerificationDialog(pubKey, null) onClosed: destroy() } } diff --git a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml index b977df8497d..608a106fac5 100644 --- a/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Communities/views/CommunitySettingsView.qml @@ -239,7 +239,7 @@ StatusSectionLayout { StatusQUtils.Utils.filterXSS(item.introMessage), StatusQUtils.Utils.filterXSS(item.outroMessage), item.options.requestToJoinEnabled ? Constants.communityChatOnRequestAccess - : Constants.communityChatPublicAccess, + : Constants.communityChatPublicAccess, item.color.toString().toUpperCase(), item.selectedTags, Utils.getImageAndCropInfoJson(item.logoImagePath, item.logoCropRect), @@ -298,7 +298,7 @@ StatusSectionLayout { declinedMembersModel: root.declinedMembers editable: root.isAdmin || root.isOwner || root.isTokenMasterOwner - memberRole: community.memberRole + memberRole: root.community.memberRole communityName: root.community.name onKickUserClicked: root.rootStore.removeUserFromCommunity(id) diff --git a/ui/imports/shared/views/chat/MessageView.qml b/ui/imports/shared/views/chat/MessageView.qml index feb19b3c36d..22bfdd43a2a 100644 --- a/ui/imports/shared/views/chat/MessageView.qml +++ b/ui/imports/shared/views/chat/MessageView.qml @@ -156,7 +156,7 @@ Loader { property bool isMessage: isEmoji || isImage || isSticker || isText || isAudio || messageContentType === Constants.messageContentType.communityInviteType || messageContentType === Constants.messageContentType.transactionType - function openProfileContextMenu(sender, mouse, isReply = false) { + function openProfileContextMenu(sender, isReply = false) { if (isViewMemberMessagesePopup) return false @@ -786,21 +786,10 @@ Loader { Global.activateDeepLink(link) } - onProfilePictureClicked: { - root.openProfileContextMenu(sender, mouse) - } - - onReplyProfileClicked: { - root.openProfileContextMenu(sender, mouse, true) - } - - onReplyMessageClicked: { - root.messageStore.messageModule.jumpToMessage(root.responseToMessageWithId) - } - - onSenderNameClicked: { - root.openProfileContextMenu(sender, mouse) - } + onProfilePictureClicked: (sender, mouse) => root.openProfileContextMenu(sender) + onReplyProfileClicked: (sender, mouse) => root.openProfileContextMenu(sender, true) + onReplyMessageClicked: (mouse) => root.messageStore.messageModule.jumpToMessage(root.responseToMessageWithId) + onSenderNameClicked: (sender) => root.openProfileContextMenu(sender) onToggleReactionClicked: { if (root.isChatBlocked)