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.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 {}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ FocusScope {
property alias titleRowComponentLoader: loader
property list<Item> 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

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
LoadingAnimation 1.0 LoadingAnimation.qml
MacTrafficLights 1.0 MacTrafficLights.qml
UserSearchFilterContainer 1.0 UserSearchFilterContainer.qml
UserSearchFilter 1.0 UserSearchFilter.qml