ContactView: indpendent lists per tab

This commit is contained in:
Michał Cieślak 2025-01-13 10:08:34 +01:00 committed by Lukáš Tinkl
parent 3281e841db
commit 52c3d1bcc9
9 changed files with 209 additions and 212 deletions

View File

@ -1,23 +1,16 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ 0.1 import StatusQ 0.1
import Models 1.0 import Models 1.0
import Storybook 1.0 import Storybook 1.0
import SortFilterProxyModel 0.2
import utils 1.0
import shared.stores 1.0 as SharedStores import shared.stores 1.0 as SharedStores
import AppLayouts.Profile.views 1.0 import AppLayouts.Profile.views 1.0
import AppLayouts.Profile.stores 1.0 import AppLayouts.Profile.stores 1.0
import mainui.adaptors 1.0 import mainui.adaptors 1.0
Item { Item {
id: root
ContactsView { ContactsView {
sectionTitle: "Contacts" sectionTitle: "Contacts"
anchors.fill: parent anchors.fill: parent
@ -29,13 +22,20 @@ Item {
function joinPrivateChat(pubKey) {} function joinPrivateChat(pubKey) {}
function acceptContactRequest(pubKey, contactRequestId) {} function acceptContactRequest(pubKey, contactRequestId) {}
function dismissContactRequest(pubKey, contactRequestId) {} function dismissContactRequest(pubKey, contactRequestId) {}
function resolveENS(value) {}
signal resolvedENS(string resolvedPubKey, string resolvedAddress,
string uuid)
} }
utilsStore: SharedStores.UtilsStore { utilsStore: SharedStores.UtilsStore {
function getEmojiHash(publicKey) { function getEmojiHash(publicKey) {
if (publicKey === "") if (publicKey === "")
return "" return ""
return JSON.stringify(["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻", "📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"]) return JSON.stringify(
["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻",
"📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"])
} }
} }
@ -47,20 +47,8 @@ Item {
ContactsModelAdaptor { ContactsModelAdaptor {
id: adaptor id: adaptor
allContacts: SortFilterProxyModel {
sourceModel: UsersModel {}
proxyRoles: [
FastExpressionRole {
function displayNameProxy(localNickname, ensName, displayName, aliasName) {
return ProfileUtils.displayName(localNickname, ensName, displayName, aliasName)
}
name: "preferredDisplayName" allContacts: UsersModel {}
expectedRoles: ["localNickname", "displayName", "ensName", "alias"]
expression: displayNameProxy(model.localNickname, model.ensName, model.displayName, model.alias)
}
]
}
} }
} }

View File

@ -18,6 +18,8 @@ ListModel {
alias: "", alias: "",
localNickname: "", localNickname: "",
ensName: "", ensName: "",
preferredDisplayName: "Mike has a very long name that should elide " +
"eventually and result in a tooltip displayed instead",
icon: ModelsData.icons.cryptPunks, icon: ModelsData.icons.cryptPunks,
colorId: 7, colorId: 7,
isEnsVerified: false, isEnsVerified: false,
@ -43,6 +45,7 @@ ListModel {
alias: "", alias: "",
localNickname: "", localNickname: "",
ensName: "", ensName: "",
preferredDisplayName: "Jane",
icon: "", icon: "",
colorId: 9, colorId: 9,
isEnsVerified: false, isEnsVerified: false,
@ -67,6 +70,7 @@ ListModel {
alias: "", alias: "",
localNickname: "Johnny Johny", localNickname: "Johnny Johny",
ensName: "", ensName: "",
preferredDisplayName: "Johnny Johny",
icon: ModelsData.icons.dragonereum, icon: ModelsData.icons.dragonereum,
colorId: 4, colorId: 4,
isEnsVerified: false, isEnsVerified: false,
@ -91,6 +95,7 @@ ListModel {
alias: "meth", alias: "meth",
localNickname: "", localNickname: "",
ensName: "", ensName: "",
preferredDisplayName: "Maria",
icon: "", icon: "",
colorId: 5, colorId: 5,
isEnsVerified: false, isEnsVerified: false,
@ -112,6 +117,7 @@ ListModel {
alias: "Richard The Lionheart", alias: "Richard The Lionheart",
localNickname: "", localNickname: "",
ensName: "richard-the-lionheart.eth", ensName: "richard-the-lionheart.eth",
preferredDisplayName: "richard-the-lionheart.eth",
icon: "", icon: "",
colorId: 3, colorId: 3,
isEnsVerified: true, isEnsVerified: true,
@ -132,6 +138,7 @@ ListModel {
alias: "", alias: "",
localNickname: "", localNickname: "",
ensName: "8⃣6⃣.sth.eth", ensName: "8⃣6⃣.sth.eth",
preferredDisplayName: "8⃣6⃣.sth.eth",
icon: "", icon: "",
colorId: 7, colorId: 7,
isEnsVerified: true, isEnsVerified: true,

View File

@ -58,18 +58,14 @@ Item {
model: SortFilterProxyModel { model: SortFilterProxyModel {
sourceModel: root.model sourceModel: root.model
sorters: [ sorters: StringSorter {
StringSorter { roleName: "preferredDisplayName"
roleName: "preferredDisplayName" caseSensitivity: Qt.CaseInsensitive
caseSensitivity: Qt.CaseInsensitive }
}
]
filters: [ filters: UserSearchFilter {
UserSearchFilterContainer { searchString: root.searchString
searchString: root.searchString }
}
]
} }
spacing: 0 spacing: 0

View File

@ -2,70 +2,44 @@ import QtQuick 2.15
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import shared 1.0 import shared.views 1.0
import utils 1.0 import utils 1.0
import SortFilterProxyModel 0.2
StatusListView { StatusListView {
id: root id: root
required property var contactsModel property bool inviteButtonVisible
property int panelUsage: Constants.contactsPanelUsage.unknownPosition
property string searchString: ""
signal openContactContextMenu(string publicKey) signal profilePopupRequested(string publicKey)
signal sendMessageActionTriggered(string publicKey) signal contextMenuRequested(string publicKey)
signal contactRequestAccepted(string publicKey)
signal contactRequestRejected(string publicKey) signal sendMessageRequested(string publicKey)
signal acceptContactRequested(string publicKey)
signal rejectContactRequested(string publicKey)
objectName: "ContactListPanel_ListView" objectName: "ContactListPanel_ListView"
model: SortFilterProxyModel { header: NoFriendsRectangle {
id: filteredModel width: ListView.view.width
visible: ListView.view.count === 0
sourceModel: root.contactsModel inviteButtonVisible: root.inviteButtonVisible
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
}
]
} }
delegate: ContactPanel { delegate: ContactPanel {
width: ListView.view.width width: ListView.view.width
showSendMessageButton: model.isContact && !model.isBlocked showSendMessageButton: model.isContact && !model.isBlocked
showRejectContactRequestButton: root.panelUsage === Constants.contactsPanelUsage.pendingContacts && showRejectContactRequestButton:
model.contactRequest === Constants.ContactRequestState.Received model.contactRequest === Constants.ContactRequestState.Received
showAcceptContactRequestButton: showRejectContactRequestButton showAcceptContactRequestButton: showRejectContactRequestButton
contactText: root.panelUsage === Constants.contactsPanelUsage.pendingContacts && contactText: model.contactRequest === Constants.ContactRequestState.Sent
model.contactRequest === Constants.ContactRequestState.Sent ? qsTr("Contact Request Sent") ? qsTr("Contact Request Sent") : ""
: ""
onClicked: Global.openProfilePopup(model.pubKey) onClicked: root.profilePopupRequested(model.pubKey)
onContextMenuRequested: root.openContactContextMenu(model.pubKey) onContextMenuRequested: root.contextMenuRequested(model.pubKey)
onSendMessageRequested: root.sendMessageActionTriggered(model.pubKey) onSendMessageRequested: root.sendMessageRequested(model.pubKey)
onAcceptContactRequested: root.contactRequestAccepted(model.pubKey) onAcceptContactRequested: root.acceptContactRequested(model.pubKey)
onRejectRequestRequested: root.contactRequestRejected(model.pubKey) onRejectRequestRequested: root.rejectContactRequested(model.pubKey)
} }
} }

View File

@ -1,26 +1,25 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import StatusQ 0.1 import StatusQ 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 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.controls 1.0
import shared.panels 1.0
import shared.popups 1.0
import shared.stores 1.0 as SharedStores import shared.stores 1.0 as SharedStores
import shared.views 1.0 import shared.views 1.0
import shared.views.chat 1.0 import shared.views.chat 1.0
import AppLayouts.Profile.stores 1.0 import utils 1.0
import AppLayouts.Profile.panels 1.0
import AppLayouts.Profile.popups 1.0 import SortFilterProxyModel 0.2
SettingsContentBase { SettingsContentBase {
id: root id: root
@ -37,6 +36,7 @@ SettingsContentBase {
titleRowComponentLoader.sourceComponent: StatusButton { titleRowComponentLoader.sourceComponent: StatusButton {
objectName: "ContactsView_ContactRequest_Button" objectName: "ContactsView_ContactRequest_Button"
text: qsTr("Send contact request to chat key") text: qsTr("Send contact request to chat key")
onClicked: sendContactRequestComponent.createObject(root).open() onClicked: sendContactRequestComponent.createObject(root).open()
} }
@ -70,128 +70,134 @@ SettingsContentBase {
StatusTabBar { StatusTabBar {
id: contactsTabBar id: contactsTabBar
Layout.fillWidth: true Layout.fillWidth: true
StatusTabButton { StatusTabButton {
readonly property int panelUsage: Constants.contactsPanelUsage.mutualContacts objectName: "ContactsView_Contacts_Button"
width: implicitWidth width: implicitWidth
text: qsTr("Contacts") text: qsTr("Contacts")
} }
StatusTabButton { StatusTabButton {
readonly property int panelUsage: Constants.contactsPanelUsage.pendingContacts
objectName: "ContactsView_PendingRequest_Button" objectName: "ContactsView_PendingRequest_Button"
width: implicitWidth width: implicitWidth
enabled: !!root.pendingContactsModel && !root.pendingContactsModel.ModelCount.empty enabled: !root.pendingContactsModel.ModelCount.empty
text: qsTr("Pending Requests") text: qsTr("Pending Requests")
badge.value: root.pendingReceivedContactsCount badge.value: root.pendingReceivedContactsCount
} }
StatusTabButton { StatusTabButton {
readonly property int panelUsage: Constants.contactsPanelUsage.blockedContacts
objectName: "ContactsView_Blocked_Button" objectName: "ContactsView_Blocked_Button"
width: implicitWidth width: implicitWidth
enabled: !!root.blockedContactsModel && !root.blockedContactsModel.ModelCount.empty enabled: !root.blockedContactsModel.ModelCount.empty
text: qsTr("Blocked") text: qsTr("Blocked")
} }
} }
SearchBox { SearchBox {
id: searchBox id: searchBox
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: qsTr("Search by name or chat key") placeholderText: qsTr("Search by name or chat key")
} }
} }
ContactsListPanel { StackLayout {
id: contactsListPanel
width: root.contentWidth width: root.contentWidth
height: root.availableHeight height: root.availableHeight
panelUsage: contactsTabBar.currentItem.panelUsage currentIndex: contactsTabBar.currentIndex
contactsModel: {
switch (panelUsage) { ContactsList {
case Constants.contactsPanelUsage.pendingContacts: inviteButtonVisible: searchBox.text === ""
return root.pendingContactsModel
case Constants.contactsPanelUsage.blockedContacts: model: SortFilterProxyModel {
return root.blockedContactsModel sourceModel: root.mutualContactsModel
case Constants.contactsPanelUsage.mutualContacts:
default: filters: UserSearchFilter {
return root.mutualContactsModel searchString: searchBox.text
}
sorters: [
RoleSorter {
roleName: "isVerified"
sortOrder: Qt.DescendingOrder
},
StringSorter {
roleName: "preferredDisplayName"
caseSensitivity: Qt.CaseInsensitive
}
]
} }
}
section.property: { section.property: "isVerified"
switch (contactsListPanel.panelUsage) { section.delegate: SectionComponent {
case Constants.contactsPanelUsage.pendingContacts: text: section === "true" ? qsTr("Trusted Contacts")
return "contactRequest" : qsTr("Contacts")
case Constants.contactsPanelUsage.mutualContacts:
return "isVerified"
case Constants.contactsPanelUsage.blockedContacts:
default:
return ""
} }
section.labelPositioning: ViewSection.InlineLabels |
ViewSection.CurrentLabelAtStart
} }
section.delegate: SectionComponent {
text: { ContactsList {
switch (contactsListPanel.panelUsage) { model: SortFilterProxyModel {
case Constants.contactsPanelUsage.pendingContacts: sourceModel: root.pendingContactsModel
return section === `${Constants.ContactRequestState.Received}` ? qsTr("Received") : qsTr("Sent")
case Constants.contactsPanelUsage.mutualContacts: filters: UserSearchFilter {
return section === "true" ? qsTr("Trusted Contacts") : qsTr("Contacts") searchString: searchBox.text
case Constants.contactsPanelUsage.blockedContacts: }
default:
return "" 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 { component ContactsList: ContactsListPanel {
width: ListView.view.width onProfilePopupRequested: Global.openProfilePopup(publicKey)
visible: ListView.view.count === 0 onContextMenuRequested: root.openContextMenu(model, publicKey)
inviteButtonVisible: searchBox.text === ""
}
searchString: searchBox.text onSendMessageRequested: root.contactsStore.joinPrivateChat(publicKey)
onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey) onAcceptContactRequested: root.contactsStore.acceptContactRequest(publicKey, "")
onSendMessageActionTriggered: root.contactsStore.joinPrivateChat(publicKey) onRejectContactRequested: root.contactsStore.dismissContactRequest(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()
}
}
} }
component SectionComponent: Rectangle { component SectionComponent: Rectangle {
@ -215,4 +221,41 @@ SettingsContentBase {
elide: Text.ElideRight 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()
}
}
} }

View File

@ -20,7 +20,7 @@ FocusScope {
property alias titleRowComponentLoader: loader property alias titleRowComponentLoader: loader
property list<Item> headerComponents property list<Item> headerComponents
property alias bottomHeaderComponents: secondHeaderRow.contentItem property alias bottomHeaderComponents: secondHeaderRow.contentItem
default property alias content: contentWrapper.children default property alias content: contentWrapper.data
property alias titleLayout: titleLayout property alias titleLayout: titleLayout
property bool stickTitleRowComponentLoader: false property bool stickTitleRowComponentLoader: false

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -3,4 +3,4 @@ module shared
DelegateModelGeneralized 1.0 DelegateModelGeneralized.qml DelegateModelGeneralized 1.0 DelegateModelGeneralized.qml
LoadingAnimation 1.0 LoadingAnimation.qml LoadingAnimation 1.0 LoadingAnimation.qml
MacTrafficLights 1.0 MacTrafficLights.qml MacTrafficLights 1.0 MacTrafficLights.qml
UserSearchFilterContainer 1.0 UserSearchFilterContainer.qml UserSearchFilter 1.0 UserSearchFilter.qml