From 52c3d1bcc957c928ac96e99f71d22bf6ea42a7b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Mon, 13 Jan 2025 10:08:34 +0100 Subject: [PATCH] ContactView: indpendent lists per tab --- storybook/pages/ContactsViewPage.qml | 30 +-- storybook/src/Models/UsersModel.qml | 7 + .../Communities/panels/MembersTabPanel.qml | 18 +- .../Profile/panels/ContactsListPanel.qml | 68 ++---- .../AppLayouts/Profile/views/ContactsView.qml | 227 +++++++++++------- .../Profile/views/SettingsContentBase.qml | 2 +- ui/imports/shared/UserSearchFilter.qml | 28 +++ .../shared/UserSearchFilterContainer.qml | 39 --- ui/imports/shared/qmldir | 2 +- 9 files changed, 209 insertions(+), 212 deletions(-) create mode 100644 ui/imports/shared/UserSearchFilter.qml delete mode 100644 ui/imports/shared/UserSearchFilterContainer.qml diff --git a/storybook/pages/ContactsViewPage.qml b/storybook/pages/ContactsViewPage.qml index 61fc2da825..ff537aa226 100644 --- a/storybook/pages/ContactsViewPage.qml +++ b/storybook/pages/ContactsViewPage.qml @@ -1,23 +1,16 @@ import QtQuick 2.15 -import QtQuick.Controls 2.15 import StatusQ 0.1 import Models 1.0 import Storybook 1.0 -import SortFilterProxyModel 0.2 - -import utils 1.0 - import shared.stores 1.0 as SharedStores import AppLayouts.Profile.views 1.0 import AppLayouts.Profile.stores 1.0 import mainui.adaptors 1.0 Item { - id: root - ContactsView { sectionTitle: "Contacts" anchors.fill: parent @@ -29,13 +22,20 @@ Item { function joinPrivateChat(pubKey) {} function acceptContactRequest(pubKey, contactRequestId) {} function dismissContactRequest(pubKey, contactRequestId) {} + + function resolveENS(value) {} + + signal resolvedENS(string resolvedPubKey, string resolvedAddress, + string uuid) } utilsStore: SharedStores.UtilsStore { function getEmojiHash(publicKey) { if (publicKey === "") return "" - return JSON.stringify(["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻", "📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"]) + return JSON.stringify( + ["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻", + "📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"]) } } @@ -47,20 +47,8 @@ Item { ContactsModelAdaptor { id: adaptor - allContacts: SortFilterProxyModel { - sourceModel: UsersModel {} - proxyRoles: [ - FastExpressionRole { - function displayNameProxy(localNickname, ensName, displayName, aliasName) { - return ProfileUtils.displayName(localNickname, ensName, displayName, aliasName) - } - name: "preferredDisplayName" - expectedRoles: ["localNickname", "displayName", "ensName", "alias"] - expression: displayNameProxy(model.localNickname, model.ensName, model.displayName, model.alias) - } - ] - } + allContacts: UsersModel {} } } diff --git a/storybook/src/Models/UsersModel.qml b/storybook/src/Models/UsersModel.qml index 03c6e6fcf1..1489838861 100644 --- a/storybook/src/Models/UsersModel.qml +++ b/storybook/src/Models/UsersModel.qml @@ -18,6 +18,8 @@ ListModel { alias: "", localNickname: "", ensName: "", + preferredDisplayName: "Mike has a very long name that should elide " + + "eventually and result in a tooltip displayed instead", icon: ModelsData.icons.cryptPunks, colorId: 7, isEnsVerified: false, @@ -43,6 +45,7 @@ ListModel { alias: "", localNickname: "", ensName: "", + preferredDisplayName: "Jane", icon: "", colorId: 9, isEnsVerified: false, @@ -67,6 +70,7 @@ ListModel { alias: "", localNickname: "Johnny Johny", ensName: "", + preferredDisplayName: "Johnny Johny", icon: ModelsData.icons.dragonereum, colorId: 4, isEnsVerified: false, @@ -91,6 +95,7 @@ ListModel { alias: "meth", localNickname: "", ensName: "", + preferredDisplayName: "Maria", icon: "", colorId: 5, isEnsVerified: false, @@ -112,6 +117,7 @@ ListModel { alias: "Richard The Lionheart", localNickname: "", ensName: "richard-the-lionheart.eth", + preferredDisplayName: "richard-the-lionheart.eth", icon: "", colorId: 3, isEnsVerified: true, @@ -132,6 +138,7 @@ ListModel { alias: "", localNickname: "", ensName: "8⃣6⃣.sth.eth", + preferredDisplayName: "8⃣6⃣.sth.eth", icon: "", colorId: 7, isEnsVerified: true, diff --git a/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml b/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml index 35d91e085d..45e46085b7 100644 --- a/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml @@ -58,18 +58,14 @@ Item { model: SortFilterProxyModel { sourceModel: root.model - sorters: [ - StringSorter { - roleName: "preferredDisplayName" - caseSensitivity: Qt.CaseInsensitive - } - ] + sorters: StringSorter { + roleName: "preferredDisplayName" + caseSensitivity: Qt.CaseInsensitive + } - filters: [ - UserSearchFilterContainer { - searchString: root.searchString - } - ] + filters: UserSearchFilter { + searchString: root.searchString + } } spacing: 0 diff --git a/ui/app/AppLayouts/Profile/panels/ContactsListPanel.qml b/ui/app/AppLayouts/Profile/panels/ContactsListPanel.qml index 5db8010b6a..57b47a5288 100644 --- a/ui/app/AppLayouts/Profile/panels/ContactsListPanel.qml +++ b/ui/app/AppLayouts/Profile/panels/ContactsListPanel.qml @@ -2,70 +2,44 @@ import QtQuick 2.15 import StatusQ.Core 0.1 -import shared 1.0 +import shared.views 1.0 import utils 1.0 -import SortFilterProxyModel 0.2 - StatusListView { id: root - required property var contactsModel - property int panelUsage: Constants.contactsPanelUsage.unknownPosition - property string searchString: "" + property bool inviteButtonVisible - signal openContactContextMenu(string publicKey) - signal sendMessageActionTriggered(string publicKey) - signal contactRequestAccepted(string publicKey) - signal contactRequestRejected(string publicKey) + signal profilePopupRequested(string publicKey) + signal contextMenuRequested(string publicKey) + + signal sendMessageRequested(string publicKey) + signal acceptContactRequested(string publicKey) + signal rejectContactRequested(string publicKey) objectName: "ContactListPanel_ListView" - model: SortFilterProxyModel { - id: filteredModel - - sourceModel: root.contactsModel - - filters: [ - UserSearchFilterContainer { - searchString: root.searchString - } - ] - - sorters: [ - FilterSorter { // Trusted contacts first - enabled: root.panelUsage === Constants.contactsPanelUsage.mutualContacts - ValueFilter { roleName: "isVerified"; value: true } - }, - FilterSorter { // Received CRs first - id: pendingFilter - readonly property int received: Constants.ContactRequestState.Received - enabled: root.panelUsage === Constants.contactsPanelUsage.pendingContacts - ValueFilter { roleName: "contactRequest"; value: pendingFilter.received } - }, - StringSorter { - roleName: "preferredDisplayName" - caseSensitivity: Qt.CaseInsensitive - } - ] + header: NoFriendsRectangle { + width: ListView.view.width + visible: ListView.view.count === 0 + inviteButtonVisible: root.inviteButtonVisible } delegate: ContactPanel { width: ListView.view.width showSendMessageButton: model.isContact && !model.isBlocked - showRejectContactRequestButton: root.panelUsage === Constants.contactsPanelUsage.pendingContacts && - model.contactRequest === Constants.ContactRequestState.Received + showRejectContactRequestButton: + model.contactRequest === Constants.ContactRequestState.Received showAcceptContactRequestButton: showRejectContactRequestButton - contactText: root.panelUsage === Constants.contactsPanelUsage.pendingContacts && - model.contactRequest === Constants.ContactRequestState.Sent ? qsTr("Contact Request Sent") - : "" + contactText: model.contactRequest === Constants.ContactRequestState.Sent + ? qsTr("Contact Request Sent") : "" - onClicked: Global.openProfilePopup(model.pubKey) - onContextMenuRequested: root.openContactContextMenu(model.pubKey) - onSendMessageRequested: root.sendMessageActionTriggered(model.pubKey) - onAcceptContactRequested: root.contactRequestAccepted(model.pubKey) - onRejectRequestRequested: root.contactRequestRejected(model.pubKey) + onClicked: root.profilePopupRequested(model.pubKey) + onContextMenuRequested: root.contextMenuRequested(model.pubKey) + onSendMessageRequested: root.sendMessageRequested(model.pubKey) + onAcceptContactRequested: root.acceptContactRequested(model.pubKey) + onRejectRequestRequested: root.rejectContactRequested(model.pubKey) } } diff --git a/ui/app/AppLayouts/Profile/views/ContactsView.qml b/ui/app/AppLayouts/Profile/views/ContactsView.qml index bea24fb529..693bf2568e 100644 --- a/ui/app/AppLayouts/Profile/views/ContactsView.qml +++ b/ui/app/AppLayouts/Profile/views/ContactsView.qml @@ -1,26 +1,25 @@ import QtQuick 2.15 -import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import StatusQ 0.1 -import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 -import utils 1.0 +import AppLayouts.Profile.panels 1.0 +import AppLayouts.Profile.popups 1.0 +import AppLayouts.Profile.stores 1.0 +import shared 1.0 import shared.controls 1.0 -import shared.panels 1.0 -import shared.popups 1.0 import shared.stores 1.0 as SharedStores import shared.views 1.0 import shared.views.chat 1.0 -import AppLayouts.Profile.stores 1.0 -import AppLayouts.Profile.panels 1.0 -import AppLayouts.Profile.popups 1.0 +import utils 1.0 + +import SortFilterProxyModel 0.2 SettingsContentBase { id: root @@ -37,6 +36,7 @@ SettingsContentBase { titleRowComponentLoader.sourceComponent: StatusButton { objectName: "ContactsView_ContactRequest_Button" + text: qsTr("Send contact request to chat key") onClicked: sendContactRequestComponent.createObject(root).open() } @@ -70,128 +70,134 @@ SettingsContentBase { StatusTabBar { id: contactsTabBar + Layout.fillWidth: true StatusTabButton { - readonly property int panelUsage: Constants.contactsPanelUsage.mutualContacts + objectName: "ContactsView_Contacts_Button" width: implicitWidth text: qsTr("Contacts") } StatusTabButton { - readonly property int panelUsage: Constants.contactsPanelUsage.pendingContacts - objectName: "ContactsView_PendingRequest_Button" + width: implicitWidth - enabled: !!root.pendingContactsModel && !root.pendingContactsModel.ModelCount.empty + enabled: !root.pendingContactsModel.ModelCount.empty text: qsTr("Pending Requests") badge.value: root.pendingReceivedContactsCount } StatusTabButton { - readonly property int panelUsage: Constants.contactsPanelUsage.blockedContacts - objectName: "ContactsView_Blocked_Button" + width: implicitWidth - enabled: !!root.blockedContactsModel && !root.blockedContactsModel.ModelCount.empty + enabled: !root.blockedContactsModel.ModelCount.empty text: qsTr("Blocked") } } SearchBox { id: searchBox + Layout.fillWidth: true placeholderText: qsTr("Search by name or chat key") } } - ContactsListPanel { - id: contactsListPanel + StackLayout { width: root.contentWidth height: root.availableHeight - panelUsage: contactsTabBar.currentItem.panelUsage - contactsModel: { - switch (panelUsage) { - case Constants.contactsPanelUsage.pendingContacts: - return root.pendingContactsModel - case Constants.contactsPanelUsage.blockedContacts: - return root.blockedContactsModel - case Constants.contactsPanelUsage.mutualContacts: - default: - return root.mutualContactsModel + currentIndex: contactsTabBar.currentIndex + + ContactsList { + inviteButtonVisible: searchBox.text === "" + + model: SortFilterProxyModel { + sourceModel: root.mutualContactsModel + + filters: UserSearchFilter { + searchString: searchBox.text + } + + sorters: [ + RoleSorter { + roleName: "isVerified" + sortOrder: Qt.DescendingOrder + }, + StringSorter { + roleName: "preferredDisplayName" + caseSensitivity: Qt.CaseInsensitive + } + ] } - } - section.property: { - switch (contactsListPanel.panelUsage) { - case Constants.contactsPanelUsage.pendingContacts: - return "contactRequest" - case Constants.contactsPanelUsage.mutualContacts: - return "isVerified" - case Constants.contactsPanelUsage.blockedContacts: - default: - return "" + + section.property: "isVerified" + section.delegate: SectionComponent { + text: section === "true" ? qsTr("Trusted Contacts") + : qsTr("Contacts") } + + section.labelPositioning: ViewSection.InlineLabels | + ViewSection.CurrentLabelAtStart } - section.delegate: SectionComponent { - text: { - switch (contactsListPanel.panelUsage) { - case Constants.contactsPanelUsage.pendingContacts: - return section === `${Constants.ContactRequestState.Received}` ? qsTr("Received") : qsTr("Sent") - case Constants.contactsPanelUsage.mutualContacts: - return section === "true" ? qsTr("Trusted Contacts") : qsTr("Contacts") - case Constants.contactsPanelUsage.blockedContacts: - default: - return "" + + ContactsList { + model: SortFilterProxyModel { + sourceModel: root.pendingContactsModel + + filters: UserSearchFilter { + searchString: searchBox.text + } + + sorters: [ + FilterSorter { // Received CRs first + ValueFilter { + roleName: "contactRequest" + value: Constants.ContactRequestState.Received + } + }, + + StringSorter { + roleName: "preferredDisplayName" + caseSensitivity: Qt.CaseInsensitive + } + ] + } + + section.property: "contactRequest" + section.delegate: SectionComponent { + text: section === `${Constants.ContactRequestState.Received}` + ? qsTr("Received") : qsTr("Sent") + } + + section.labelPositioning: ViewSection.InlineLabels | + ViewSection.CurrentLabelAtStart + } + + ContactsList { + model: SortFilterProxyModel { + sourceModel: root.blockedContactsModel + + filters: UserSearchFilter { + searchString: searchBox.text + } + + sorters: StringSorter { + roleName: "preferredDisplayName" + caseSensitivity: Qt.CaseInsensitive } } } - section.labelPositioning: ViewSection.InlineLabels | ViewSection.CurrentLabelAtStart + } - header: NoFriendsRectangle { - width: ListView.view.width - visible: ListView.view.count === 0 - inviteButtonVisible: searchBox.text === "" - } + component ContactsList: ContactsListPanel { + onProfilePopupRequested: Global.openProfilePopup(publicKey) + onContextMenuRequested: root.openContextMenu(model, publicKey) - searchString: searchBox.text - onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey) - onSendMessageActionTriggered: root.contactsStore.joinPrivateChat(publicKey) - onContactRequestAccepted: root.contactsStore.acceptContactRequest(publicKey, "") - onContactRequestRejected: root.contactsStore.dismissContactRequest(publicKey, "") - - Component { - id: sendContactRequestComponent - SendContactRequestModal { - contactsStore: root.contactsStore - onClosed: destroy() - } - } - - Component { - id: contactContextMenuComponent - ProfileContextMenu { - id: contactContextMenu - - property string pubKey - - onOpenProfileClicked: Global.openProfilePopup(contactContextMenu.pubKey, null, null) - onReviewContactRequest: Global.openReviewContactRequestPopup(contactContextMenu.pubKey, null) - onSendContactRequest: Global.openContactRequestPopup(contactContextMenu.pubKey, null) - onEditNickname: Global.openNicknamePopupRequested(contactContextMenu.pubKey, null) - onUnblockContact: Global.unblockContactRequested(contactContextMenu.pubKey) - onMarkAsUntrusted: Global.markAsUntrustedRequested(contactContextMenu.pubKey) - onRemoveContact: Global.removeContactRequested(contactContextMenu.pubKey) - onBlockContact: Global.blockContactRequested(contactContextMenu.pubKey) - - onCreateOneToOneChat: root.contactsStore.joinPrivateChat(contactContextMenu.pubKey) - onRemoveTrustStatus: root.contactsStore.removeTrustStatus(contactContextMenu.pubKey) - onRemoveNickname: root.contactsStore.changeContactNickname(contactContextMenu.pubKey, "", - contactContextMenu.displayName, true) - onMarkAsTrusted: Global.openMarkAsIDVerifiedPopup(contactContextMenu.pubKey, null) - onRemoveTrustedMark: Global.openRemoveIDVerificationDialog(contactContextMenu.pubKey, null) - onClosed: destroy() - } - } + onSendMessageRequested: root.contactsStore.joinPrivateChat(publicKey) + onAcceptContactRequested: root.contactsStore.acceptContactRequest(publicKey, "") + onRejectContactRequested: root.contactsStore.dismissContactRequest(publicKey, "") } component SectionComponent: Rectangle { @@ -215,4 +221,41 @@ SettingsContentBase { elide: Text.ElideRight } } + + Component { + id: sendContactRequestComponent + + SendContactRequestModal { + contactsStore: root.contactsStore + onClosed: destroy() + } + } + + Component { + id: contactContextMenuComponent + + ProfileContextMenu { + id: menu + + property string pubKey + + onOpenProfileClicked: Global.openProfilePopup(menu.pubKey, null, null) + onReviewContactRequest: Global.openReviewContactRequestPopup(menu.pubKey, null) + onSendContactRequest: Global.openContactRequestPopup(menu.pubKey, null) + onEditNickname: Global.openNicknamePopupRequested(menu.pubKey, null) + onUnblockContact: Global.unblockContactRequested(menu.pubKey) + onMarkAsUntrusted: Global.markAsUntrustedRequested(menu.pubKey) + onRemoveContact: Global.removeContactRequested(menu.pubKey) + onBlockContact: Global.blockContactRequested(menu.pubKey) + + onCreateOneToOneChat: root.contactsStore.joinPrivateChat(menu.pubKey) + onRemoveTrustStatus: root.contactsStore.removeTrustStatus(menu.pubKey) + onRemoveNickname: root.contactsStore.changeContactNickname(menu.pubKey, "", + menu.displayName, true) + onMarkAsTrusted: Global.openMarkAsIDVerifiedPopup(menu.pubKey, null) + onRemoveTrustedMark: Global.openRemoveIDVerificationDialog(menu.pubKey, null) + + onClosed: destroy() + } + } } diff --git a/ui/app/AppLayouts/Profile/views/SettingsContentBase.qml b/ui/app/AppLayouts/Profile/views/SettingsContentBase.qml index 4c35a2a706..3a2ad3cb49 100644 --- a/ui/app/AppLayouts/Profile/views/SettingsContentBase.qml +++ b/ui/app/AppLayouts/Profile/views/SettingsContentBase.qml @@ -20,7 +20,7 @@ FocusScope { property alias titleRowComponentLoader: loader property list headerComponents property alias bottomHeaderComponents: secondHeaderRow.contentItem - default property alias content: contentWrapper.children + default property alias content: contentWrapper.data property alias titleLayout: titleLayout property bool stickTitleRowComponentLoader: false diff --git a/ui/imports/shared/UserSearchFilter.qml b/ui/imports/shared/UserSearchFilter.qml new file mode 100644 index 0000000000..f9b861a917 --- /dev/null +++ b/ui/imports/shared/UserSearchFilter.qml @@ -0,0 +1,28 @@ +import StatusQ.Core.Utils 0.1 + +import SortFilterProxyModel 0.2 + +AnyOf { + id: root + + property string searchString + + enabled: root.searchString !== "" + + // substring search for either nickname or the other primary/secondary display name + SearchFilter { + roleName: "localNickname" + searchPhrase: root.searchString + } + + SearchFilter { + roleName: "preferredDisplayName" + searchPhrase: root.searchString + } + + // exact search for the full key + ValueFilter { + roleName: "compressedPubKey" + value: root.searchString + } +} diff --git a/ui/imports/shared/UserSearchFilterContainer.qml b/ui/imports/shared/UserSearchFilterContainer.qml deleted file mode 100644 index 16e423f9e8..0000000000 --- a/ui/imports/shared/UserSearchFilterContainer.qml +++ /dev/null @@ -1,39 +0,0 @@ -import StatusQ 0.1 -import StatusQ.Core.Utils 0.1 - -import utils 1.0 - -import SortFilterProxyModel 0.2 - -AnyOf { - id: root - - property string searchString - - function searchPredicate(ensName, displayName, aliasName) { - const lowerCaseSearchString = root.searchString.toLowerCase() - const secondaryName = ProfileUtils.displayName("", ensName, displayName, aliasName) - - return secondaryName.toLowerCase().includes(lowerCaseSearchString) - } - - enabled: root.searchString !== "" - - // substring search for either nickname or the other primary/secondary display name - SearchFilter { - roleName: "localNickname" - searchPhrase: root.searchString - } - FastExpressionFilter { - expression: { - root.searchString - return root.searchPredicate(model.ensName, model.displayName, model.alias) - } - expectedRoles: ["ensName", "displayName", "alias"] - } - // exact search for the full key - ValueFilter { - roleName: "compressedPubKey" - value: root.searchString - } -} diff --git a/ui/imports/shared/qmldir b/ui/imports/shared/qmldir index f72b7c9b60..7e5b59b901 100644 --- a/ui/imports/shared/qmldir +++ b/ui/imports/shared/qmldir @@ -3,4 +3,4 @@ module shared DelegateModelGeneralized 1.0 DelegateModelGeneralized.qml LoadingAnimation 1.0 LoadingAnimation.qml MacTrafficLights 1.0 MacTrafficLights.qml -UserSearchFilterContainer 1.0 UserSearchFilterContainer.qml +UserSearchFilter 1.0 UserSearchFilter.qml