fix: Optimize ContactsView & MembersTabPanel settings pages

- removed nested ListViews inside StackLayouts, in order to reduce the
memory footprint and improve performance, and also to be able to better
manage the scrolling
- no more unrolled multiple listviews, which again hurt the performance;
now the views instantiate the delegates dynamically on the fly
- the tab bar and the search fields now stick to the top of the page,
with the users list view scrolling independently
- both views now uniformly use the common `ContactListItemDelegate`
- the received/sent CRs are now combined into one `pendingContacts`
model
- factored out common search/filter criteria into a new, separate SFPM
`UserFilterContainer` component
- fix an issue where StatusContactVerificationIcons wasn't properly
displaying the "blocked" state/icon
- fix documentation comments, removed relative imports, and updated some

Fixes #16612
Fixes #16958
This commit is contained in:
Lukáš Tinkl 2024-12-18 21:00:08 +01:00
parent 9131487638
commit 3d3a996fa2
No known key found for this signature in database
31 changed files with 729 additions and 858 deletions

View File

@ -0,0 +1,68 @@
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
anchors.leftMargin: 64
anchors.topMargin: 16
contentWidth: 560
contactsStore: ContactsStore {
function joinPrivateChat(pubKey) {}
function acceptContactRequest(pubKey, contactRequestId) {}
function dismissContactRequest(pubKey, contactRequestId) {}
}
utilsStore: SharedStores.UtilsStore {
function getEmojiHash(publicKey) {
if (publicKey === "")
return ""
return JSON.stringify(["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻", "📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"])
}
}
mutualContactsModel: adaptor.mutualContacts
blockedContactsModel: adaptor.blockedContacts
pendingContactsModel: adaptor.pendingContacts
pendingReceivedContactsCount: adaptor.pendingReceivedRequestContacts.count
}
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)
}
]
}
}
}
// category: Views
// status: good

View File

@ -8,6 +8,7 @@ import AppLayouts.Communities.panels 1.0
import AppLayouts.Chat.stores 1.0 as ChatStores
import AppLayouts.Profile.stores 1.0 as ProfileStores
import shared.stores 1.0
import utils 1.0
import Models 1.0
@ -15,8 +16,6 @@ import SortFilterProxyModel 0.2
import Storybook 1.0
import StatusQ 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
SplitView {
id: root
@ -24,46 +23,27 @@ SplitView {
orientation: Qt.Vertical
Logs { id: logs }
// Utils.globalUtilsInst mock
QtObject {
function getEmojiHashAsJson(publicKey) {
return JSON.stringify(["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻", "📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"])
}
function getColorId(publicKey) {
return SQUtils.ModelUtils.getByKey(usersModel, "pubKey", publicKey, "colorId")
}
function getCompressedPk(publicKey) { return "zx3sh" + publicKey }
function getColorHashAsJson(publicKey) {
return JSON.stringify([{colorId: 0, segmentLength: 1},
{colorId: 19, segmentLength: 2}])
}
function isCompressedPubKey(publicKey) { return true }
Component.onCompleted: {
Utils.globalUtilsInst = this
}
Component.onDestruction: {
Utils.globalUtilsInst = {}
}
}
MembersTabPanel {
id: membersTabPanelPage
SplitView.fillWidth: true
SplitView.fillHeight: true
placeholderText: "Search users"
model: usersModelWithMembershipState
panelType: viewStateSelector.currentValue
searchString: ctrlSearch.text
rootStore: ChatStores.RootStore {
contactsStore: ProfileStores.ContactsStore {
readonly property string myPublicKey: "0x000"
}
}
utilsStore: UtilsStore {
function getEmojiHash(publicKey) {
if (publicKey === "")
return ""
return JSON.stringify(["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻", "📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"])
}
}
onKickUserClicked: {
logs.logEvent("MembersTabPanel::onKickUserClicked", ["id", "name"], arguments)
@ -132,7 +112,7 @@ SplitView {
}
LogsAndControlsPanel {
SplitView.minimumHeight: 100
SplitView.minimumHeight: 200
SplitView.preferredHeight: 320
logsView.logText: logs.logText
@ -144,6 +124,7 @@ SplitView {
}
ComboBox {
Layout.preferredWidth: 300
id: viewStateSelector
textRole: "text"
valueRole: "value"
@ -155,6 +136,13 @@ SplitView {
ListElement { text: "Declined Members"; value: MembersTabPanel.TabType.DeclinedRequests }
}
}
Label { text: "Search" }
TextField {
id: ctrlSearch
Layout.preferredWidth: 300
placeholderText: "Search by member name or chat key"
}
}
}
@ -163,4 +151,6 @@ SplitView {
}
}
// category: Panels
// status: good
// https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/KubaDesktop?type=design&node-id=35909-605774&mode=design&t=KfrAekLfW5mTy68x-0

View File

@ -29,7 +29,7 @@ Item {
StatusTabButton {
width: implicitWidth
enabled: false
text: qsTr("Blocked & disabled")
text: "Blocked & disabled"
}
StatusTabButton {
width: implicitWidth

View File

@ -9,9 +9,10 @@ ListModel {
compressedPubKey: "zQ3shQBu4PRDX17vewYyvSczbTj344viTXxcMNvQLeyQsBDF4",
onlineStatus: Constants.onlineStatus.online,
isContact: true,
isBlocked: false,
isVerified: false,
isAdmin: false,
isUntrustworthy: true,
isUntrustworthy: false,
displayName: "Mike has a very long name that should elide " +
"eventually and result in a tooltip displayed instead",
alias: "",
@ -26,13 +27,15 @@ ListModel {
],
isAwaitingAddress: false,
memberRole: Constants.memberRole.none,
trustStatus: Constants.trustStatus.untrustworthy
trustStatus: Constants.trustStatus.unknown
},
{
pubKey: "0x04df12f12f12f12f1234",
compressedPubKey: "zQ3shQBAAPRDX17vewYyvSczbTj344viTXxcMNvQLeyQsBDF4",
onlineStatus: Constants.onlineStatus.inactive,
isContact: false,
contactRequest: Constants.ContactRequestState.Sent,
isBlocked: false,
isVerified: false,
isAdmin: false,
isUntrustworthy: false,
@ -49,13 +52,14 @@ ListModel {
],
isAwaitingAddress: false,
memberRole: Constants.memberRole.owner,
trustStatus: Constants.trustStatus.trusted
trustStatus: Constants.trustStatus.unknown
},
{
pubKey: "0x04d1b7cc0ef3f470f1238",
compressedPubKey: "zQ3shQ7u3PRDX17vewYyvSczbTj344viTXxcMNvQLeyQsCDF4",
onlineStatus: Constants.onlineStatus.inactive,
isContact: false,
isBlocked: true,
isVerified: false,
isAdmin: false,
isUntrustworthy: true,
@ -66,6 +70,10 @@ ListModel {
icon: ModelsData.icons.dragonereum,
colorId: 4,
isEnsVerified: false,
colorHash: [
{ colorId: 7, segmentLength: 3 },
{ colorId: 12, segmentLength: 1 }
],
isAwaitingAddress: false,
memberRole: Constants.memberRole.none,
trustStatus: Constants.trustStatus.untrustworthy
@ -75,16 +83,17 @@ ListModel {
compressedPubKey: "zQ3shQAL4PRDX17vewYyvSczbTj344viTXxcMNvQLeyQsBDF4",
onlineStatus: Constants.onlineStatus.online,
isContact: true,
isVerified: true,
isBlocked: false,
isVerified: false,
isAdmin: false,
isUntrustworthy: true,
displayName: "Maria",
alias: "meth",
localNickname: "86.eth",
ensName: "8⃣_6⃣.eth",
localNickname: "",
ensName: "",
icon: "",
colorId: 5,
isEnsVerified: true,
isEnsVerified: false,
isAwaitingAddress: false,
memberRole: Constants.memberRole.none,
trustStatus: Constants.trustStatus.untrustworthy
@ -93,8 +102,10 @@ ListModel {
pubKey: "0x04d1bed192343f470f1255",
compressedPubKey: "zQ3shQBu4PGDX17vewYyvSczbTj344viTXxcMNvQLeyQsBD1A",
onlineStatus: Constants.onlineStatus.online,
isContact: true,
isVerified: true,
isContact: false,
contactRequest: Constants.ContactRequestState.Received,
isBlocked: false,
isVerified: false,
isAdmin: true,
isUntrustworthy: true,
displayName: "",
@ -113,7 +124,8 @@ ListModel {
compressedPubKey: "zQ3shQBk4PRDX17vewYyvSczbTj344viTXxcMNvQLeyQsB994",
onlineStatus: Constants.onlineStatus.inactive,
isContact: true,
isVerified: false,
isBlocked: false,
isVerified: true,
isAdmin: false,
isUntrustworthy: false,
displayName: "",

View File

@ -1,10 +1,5 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core.Utils 0.1
import utils 1.0
Flow {
id: root

View File

@ -52,7 +52,7 @@ settingsContentBaseScrollView_ContactListPanel = {"container": mainWindow_Contac
settingsContentBaseScrollView_Item = {"container": mainWindow_ContactsView, "type": "Item", "unnamed": 1, "visible": True}
settingsContentBaseScrollView_sentRequests_ContactsListPanel = {"container": mainWindow_ContactsView, "objectName": "sentRequests_ContactsListPanel", "type": "ContactsListPanel", "visible": True}
contactsTabBar_Contacts_StatusTabButton = {"container": mainWindow_ContactsView, "id": "contactsBtn", "type": "StatusTabButton", "unnamed": 1, "visible": True}
settingsContentBaseScrollView_receivedRequests_ContactsListPanel = {"container": mainWindow_ContactsView, "objectName": "receivedRequests_ContactsListPanel", "type": "ContactsListPanel", "visible": True}
settingsContentBaseScrollView_receivedRequests_ContactsListPanel = {"container": mainWindow_ContactsView, "objectName": "ContactsListPanel", "type": "ContactsListPanel", "visible": True}
settingsContentBaseScrollView_mutualContacts_ContactsListPanel = {"container": mainWindow_ContactsView, "id": "mutualContacts", "type": "ContactsListPanel", "unnamed": 1, "visible": True}
settingsContentBaseScrollView_Invite_friends_StatusButton = {"container": mainWindow_ContactsView, "type": "StatusButton", "unnamed": 1, "visible": True}
settingsContentBaseScrollView_NoFriendsRectangle = {"container": mainWindow_ContactsView, "type": "NoFriendsRectangle", "unnamed": 1, "visible": True}

View File

@ -71,7 +71,7 @@ Row {
}
spacing: 4
visible: root.isContact || (root.trustIndicator !== StatusContactVerificationIcons.TrustedType.None)
visible: root.isContact || root.isBlocked || (root.trustIndicator !== StatusContactVerificationIcons.TrustedType.None)
HoverHandler {
id: hoverHandler
@ -104,7 +104,8 @@ Row {
// (un)trusted
StatusRoundIcon {
visible: !root.isBlocked && root.trustIndicator !== StatusContactVerificationIcons.TrustedType.None
visible: !root.isBlocked && (root.trustIndicator === StatusContactVerificationIcons.TrustedType.Untrustworthy ||
(root.isContact && trustIndicator === StatusContactVerificationIcons.TrustedType.Verified))
asset: root.trustContactIcon
}

View File

@ -58,22 +58,27 @@ ItemDelegate {
*/
property string pubKey: ""
/*!
\qmlproperty string StatusMemberListItem::isContact
\qmlproperty bool StatusMemberListItem::isContact
This property holds if the member represented is contact.
*/
property bool isContact: false
/*!
\qmlproperty string StatusMemberListItem::isVerified
\qmlproperty bool StatusMemberListItem::isVerified
This property holds if the member represented is verified contact.
*/
property bool isVerified: false
/*!
\qmlproperty string StatusMemberListItem::isUntrustworthy
\qmlproperty bool StatusMemberListItem::isUntrustworthy
This property holds if the member represented is untrustworthy.
*/
property bool isUntrustworthy: false
/*!
\qmlproperty string StatusMemberListItem::status
\qmlproperty bool StatusMemberListItem::isBlocked
This property holds if the member represented is blocked.
*/
property bool isBlocked: false
/*!
\qmlproperty int StatusMemberListItem::status
This property holds the connectivity status of the member represented.
int unknown: -1
@ -84,7 +89,7 @@ ItemDelegate {
// FIXME: move Constants.onlineStatus from status-desktop
property int status: 0
/*!
\qmlproperty string StatusMemberListItem::isAdmin
\qmlproperty bool StatusMemberListItem::isAdmin
This property holds the admin status of the member represented.
*/
property bool isAdmin: false
@ -126,7 +131,7 @@ ItemDelegate {
property alias badge: identicon.badge
/*!
\qmlsignal
\qmlsignal clicked
This signal is emitted when the StatusMemberListItem is clicked.
*/
signal clicked(var mouse)
@ -158,9 +163,9 @@ ItemDelegate {
}
}
horizontalPadding: 8
horizontalPadding: Theme.halfPadding
verticalPadding: 12
spacing: 8
spacing: Theme.halfPadding
icon.width: 32
icon.height: 32
@ -170,7 +175,7 @@ ItemDelegate {
background: Rectangle {
color: root.color
radius: 8
radius: Theme.radius
MouseArea {
anchors.fill: parent
@ -200,9 +205,8 @@ ItemDelegate {
// badge
badge.visible: true
badge.color: root.status === 1 ? Theme.palette.successColor1 : Theme.palette.baseColor1 // FIXME, see root.status
badge.anchors.top: undefined
badge.border.width: 2
badge.border.color: Theme.palette.statusListItem.backgroundColor
badge.border.color: root.hovered ? Theme.palette.statusBadge.hoverBorderColor : Theme.palette.statusBadge.borderColor
badge.implicitHeight: 12 // 8 px + 2 px * 2 borders
badge.implicitWidth: 12 // 8 px + 2 px * 2 borders
}
@ -243,7 +247,7 @@ ItemDelegate {
Layout.fillWidth: true
elide: Text.ElideRight
text: d.composeSubtitle()
font.pixelSize: 10
font.pixelSize: Theme.asideTextFontSize
color: Theme.palette.baseColor1
visible: !!text
@ -280,6 +284,7 @@ ItemDelegate {
id: statusContactVerificationIcons
StatusContactVerificationIcons {
isContact: root.isContact
isBlocked: root.isBlocked
trustIndicator: {
if (root.isVerified)
return StatusContactVerificationIcons.TrustedType.Verified

View File

@ -1,4 +1,4 @@
import QtQuick 2.14
import QtQuick 2.15
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
@ -12,4 +12,5 @@ StatusFlatRoundButton {
implicitHeight: 24
icon.color: Theme.palette.directColor9
backgroundHoverColor: "transparent"
tooltip.text: qsTr("Clear")
}

View File

@ -1,15 +1,15 @@
import QtQuick 2.14
import QtQuick 2.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
Rectangle {
id: statusFlatRoundButton
property StatusAssetSettings icon: StatusAssetSettings {
width: 23
height: 23
width: 24
height: 24
rotation: 0
color: {

View File

@ -1,6 +1,6 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Components 0.1
@ -17,8 +17,10 @@ Control {
signal toggled
padding: 4
contentItem: RowLayout {
spacing: 16
spacing: Theme.padding
StatusRoundIcon {
asset.name: root.icon
@ -26,22 +28,21 @@ Control {
ColumnLayout {
Layout.fillWidth: true
StatusBaseText {
text: root.title
color: Theme.palette.directColor1
font.pixelSize: 15
}
Item { Layout.fillWidth: true }
Layout.fillHeight: true
StatusBaseText {
Layout.fillWidth: true
text: root.title
visible: !!text
color: Theme.palette.directColor1
elide: Text.ElideRight
}
StatusBaseText {
Layout.fillWidth: true
Layout.fillHeight: true
text: root.subTitle
visible: !!text
color: Theme.palette.baseColor1
font.pixelSize: 15
lineHeight: 1.2
wrapMode: Text.WordWrap
elide: Text.ElideRight
@ -51,6 +52,7 @@ Control {
StatusSwitch {
id: switchItem
objectName: "switchItem"
padding: 0
onToggled: root.toggled()
}

View File

@ -1,13 +1,17 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Core.Theme 0.1
import AppLayouts.Communities.controls 1.0
Page {
id: root
leftPadding: 64
topPadding: 16
leftPadding: Theme.xlPadding*2
topPadding: Theme.padding
readonly property int preferredContentWidth: 560
property alias buttons: pageHeader.buttons
property alias subtitle: pageHeader.subtitle
@ -18,8 +22,8 @@ Page {
id: pageHeader
height: 44
leftPadding: 64
rightPadding: width - 560 - leftPadding
leftPadding: root.leftPadding
rightPadding: width - root.preferredContentWidth - leftPadding
title: root.title
}

View File

@ -3,7 +3,9 @@ import QtQuick.Layouts 1.15
import StatusQ 0.1
import StatusQ.Controls 0.1
import StatusQ.Core.Theme 0.1
import shared.controls 1.0
import shared.stores 1.0 as SharedStores
import utils 1.0
@ -72,14 +74,15 @@ SettingsPage {
membersTabBar.currentIndex = tabButton.TabBar.index
}
spacing: 19
spacing: Theme.padding
StatusTabBar {
id: membersTabBar
Layout.fillWidth: true
Layout.topMargin: 5
Layout.preferredWidth: root.preferredContentWidth
StatusTabButton {
readonly property int subSection: MembersTabPanel.TabType.AllMembers
id: allMembersBtn
objectName: "allMembersButton"
width: implicitWidth
@ -87,6 +90,8 @@ SettingsPage {
}
StatusTabButton {
readonly property int subSection: MembersTabPanel.TabType.PendingRequests
id: pendingRequestsBtn
objectName: "pendingRequestsButton"
width: implicitWidth
@ -95,6 +100,8 @@ SettingsPage {
}
StatusTabButton {
readonly property int subSection: MembersTabPanel.TabType.DeclinedRequests
id: declinedRequestsBtn
objectName: "declinedRequestsButton"
width: implicitWidth
@ -103,6 +110,8 @@ SettingsPage {
}
StatusTabButton {
readonly property int subSection: MembersTabPanel.TabType.BannedMembers
id: bannedBtn
objectName: "bannedButton"
width: implicitWidth
@ -111,21 +120,36 @@ SettingsPage {
}
}
StackLayout {
id: stackLayout
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: membersTabBar.currentIndex
SearchBox {
id: memberSearch
Layout.preferredWidth: root.preferredContentWidth
placeholderText: qsTr("Search by name or chat key")
enabled: membersTabBar.currentItem.enabled
}
MembersTabPanel {
model: root.membersModel
Layout.preferredWidth: root.preferredContentWidth
Layout.fillHeight: true
panelType: membersTabBar.currentItem.subSection
model: {
switch (panelType) {
case MembersTabPanel.TabType.PendingRequests:
return root.pendingMembersModel
case MembersTabPanel.TabType.DeclinedRequests:
return root.declinedMembersModel
case MembersTabPanel.TabType.BannedMembers:
return root.bannedMembersModel
case MembersTabPanel.TabType.AllMembers:
default:
return root.membersModel
}
}
searchString: memberSearch.text
rootStore: root.rootStore
utilsStore: root.utilsStore
memberRole: root.memberRole
panelType: MembersTabPanel.TabType.AllMembers
Layout.fillWidth: true
Layout.fillHeight: true
onKickUserClicked: {
kickBanPopup.mode = KickBanPopup.Mode.Kick
@ -133,59 +157,18 @@ SettingsPage {
kickBanPopup.userId = id
kickBanPopup.open()
}
onBanUserClicked: {
kickBanPopup.mode = KickBanPopup.Mode.Ban
kickBanPopup.username = name
kickBanPopup.userId = id
kickBanPopup.open()
}
onViewMemberMessagesClicked: root.viewMemberMessagesClicked(pubKey, displayName)
}
MembersTabPanel {
model: root.pendingMembersModel
rootStore: root.rootStore
utilsStore: root.utilsStore
memberRole: root.memberRole
panelType: MembersTabPanel.TabType.PendingRequests
Layout.fillWidth: true
Layout.fillHeight: true
onUnbanUserClicked: root.unbanUserClicked(id)
onAcceptRequestToJoin: root.acceptRequestToJoin(id)
onDeclineRequestToJoin: root.declineRequestToJoin(id)
}
MembersTabPanel {
model: root.declinedMembersModel
rootStore: root.rootStore
utilsStore: root.utilsStore
memberRole: root.memberRole
panelType: MembersTabPanel.TabType.DeclinedRequests
Layout.fillWidth: true
Layout.fillHeight: true
onAcceptRequestToJoin: root.acceptRequestToJoin(id)
}
MembersTabPanel {
model: root.bannedMembersModel
rootStore: root.rootStore
utilsStore: root.utilsStore
memberRole: root.memberRole
panelType: MembersTabPanel.TabType.BannedMembers
Layout.fillWidth: true
Layout.fillHeight: true
onUnbanUserClicked: root.unbanUserClicked(id)
onViewMemberMessagesClicked: root.viewMemberMessagesClicked(pubKey, displayName)
}
}
}
KickBanPopup {
id: kickBanPopup

View File

@ -10,25 +10,27 @@ import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Popups 0.1
import shared.controls 1.0
import shared 1.0
import shared.controls.chat 1.0
import shared.controls.delegates 1.0
import shared.stores 1.0 as SharedStores
import shared.views.chat 1.0
import utils 1.0
import AppLayouts.Chat.stores 1.0
import AppLayouts.Communities.layouts 1.0
import SortFilterProxyModel 0.2
Item {
id: root
property string placeholderText: qsTr("Search by member name or chat key")
property var model
required property var model
property string searchString
property RootStore rootStore
property SharedStores.UtilsStore utilsStore
property int panelType: MembersTabPanel.TabType.AllMembers
property int memberRole: Constants.memberRole.none
readonly property bool isOwner: memberRole === Constants.memberRole.owner
@ -49,69 +51,30 @@ Item {
DeclinedRequests
}
property int panelType: MembersTabPanel.TabType.AllMembers
ColumnLayout {
anchors.fill: parent
spacing: 30
SearchBox {
id: memberSearch
Layout.preferredWidth: 400
Layout.leftMargin: 12
placeholderText: root.placeholderText
enabled: !!root.model && !root.model.ModelCount.empty
}
StatusListView {
id: membersList
objectName: "CommunityMembersTabPanel_MembersListViews"
Layout.fillWidth: true
Layout.fillHeight: true
anchors.fill: parent
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 : [
sorters: [
StringSorter {
roleName: "preferredDisplayName"
caseSensitivity: Qt.CaseInsensitive
}
]
filters: AnyOf {
enabled: memberSearch.text !== ""
// substring search for either nickname or the other primary/secondary display name
SearchFilter {
roleName: "localNickname"
searchPhrase: memberSearch.text
}
FastExpressionFilter {
expression: {
memberSearch.text
return filteredModel.searchPredicate(model.ensName, model.displayName, model.alias)
}
expectedRoles: ["ensName", "displayName", "alias"]
}
// exact search for the full key
ValueFilter {
roleName: "compressedPubKey"
value: memberSearch.text
}
filters: [
UserSearchFilterContainer {
searchString: root.searchString
}
]
}
spacing: 0
delegate: StatusMemberListItem {
delegate: ContactListItemDelegate {
id: memberItem
// Buttons visibility conditions:
@ -163,7 +126,6 @@ Item {
// The admin that initited the pending state can change the state. Actions are not visible for other admins
readonly property bool ctaAllowed: !isRejectedPending && !isAcceptedPending && !isBanPending && !isUnbanPending && !isKickPending
readonly property bool isHovered: memberItem.hovered
readonly property bool canBeBanned: {
if (model.isCurrentUser)
return false
@ -179,7 +141,7 @@ Item {
default: return true
}
}
readonly property bool showOnHover: isHovered && ctaAllowed
readonly property bool showOnHover: hovered && ctaAllowed
readonly property bool canDeleteMessages: model.isCurrentUser || model.memberRole !== Constants.memberRole.owner
/// Button visibility ///
@ -206,9 +168,6 @@ Item {
isAwaitingAddress: model.membershipRequestState === Constants.CommunityMembershipRequestState.AwaitingAddress
rightPadding: 75
leftPadding: 12
components: [
StatusBaseText {
id: pendingText
@ -249,7 +208,6 @@ Item {
},
StatusButton {
id: kickButton
anchors.verticalCenter: parent.verticalCenter
objectName: "MemberListItem_KickButton"
text: qsTr("Kick")
@ -260,7 +218,6 @@ Item {
},
StatusButton {
id: banButton
objectName: "MemberListItem_BanButton"
anchors.verticalCenter: parent.verticalCenter
visible: banButtonVisible
@ -283,9 +240,10 @@ Item {
StatusButton {
id: acceptButton
anchors.verticalCenter: parent.verticalCenter
opacity: acceptButtonVisible
visible: acceptButtonVisible
text: qsTr("Accept")
type: StatusBaseButton.Type.Success
size: StatusBaseButton.Size.Small
icon.name: "checkmark-circle"
icon.color: enabled ? Theme.palette.successColor1 : disabledTextColor
loading: model.requestToJoinLoading
@ -295,9 +253,10 @@ Item {
StatusButton {
id: rejectButton
opacity: rejectButtonVisible
visible: rejectButtonVisible
text: qsTr("Reject")
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
icon.name: "close-circle"
icon.color: enabled ? Theme.palette.dangerColor1 : disabledTextColor
enabled: !rejectPendingButtonVisible
@ -307,19 +266,10 @@ Item {
readonly property string title: model.preferredDisplayName
width: membersList.width
color: "transparent"
width: ListView.view.width
pubKey: model.isEnsVerified ? "" : Utils.getElidedCompressedPk(model.pubKey)
nickName: model.localNickname
userName: ProfileUtils.displayName("", model.ensName, model.displayName, model.alias)
status: model.onlineStatus
icon.color: Utils.colorForColorId(model.colorId)
icon.name: model.icon
icon.width: 40
icon.height: 40
ringSettings.ringSpecModel: model.colorHash
badge.visible: (root.panelType === MembersTabPanel.TabType.AllMembers)
onClicked: {
if (mouse.button === Qt.RightButton) {
@ -341,14 +291,12 @@ Item {
hasLocalNickname: !!model.localNickname
}
Global.openMenu(memberContextMenuComponent, this, params)
memberContextMenuComponent.createObject(root, params).popup(this)
} else if (mouse.button === Qt.LeftButton) {
Global.openProfilePopup(model.pubKey)
}
}
}
}
}
Component {
id: memberContextMenuComponent
@ -377,6 +325,7 @@ Item {
onClosed: destroy()
}
}
}
QtObject {
id: d
@ -384,5 +333,6 @@ Item {
// so that the text aligned on all rows (the text might be different on each row)
property real pendingTextMaxWidth: 0
}
onPanelTypeChanged: { d.pendingTextMaxWidth = 0 }
}

View File

@ -2,7 +2,6 @@ import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import QtGraphicalEffects 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1

View File

@ -25,44 +25,30 @@ StatusDialog {
Kick, Ban
}
width: 400
width: 480
title: root.mode === KickBanPopup.Mode.Kick
? qsTr("Kick %1").arg(root.username)
: qsTr("Ban %1").arg(root.username)
contentItem: ColumnLayout {
anchors.centerIn: parent
StatusBaseText {
Layout.fillWidth: true
Layout.fillHeight: true
font.pixelSize: Theme.primaryTextFontSize
wrapMode: Text.Wrap
text: root.mode === KickBanPopup.Mode.Kick
? qsTr("Are you sure you want to kick <b>%1</b> from %2?")
.arg(root.username).arg(root.communityName)
: qsTr("Are you sure you want to ban <b>%1</b> from %2? This means that they will be kicked from this community and banned from re-joining.")
.arg(root.username).arg(root.communityName)
}
RowLayout {
visible: root.mode === KickBanPopup.Mode.Ban
StatusBaseText {
Layout.fillWidth: true
text: qsTr("Delete all messages posted by the user")
font.pixelSize: Theme.primaryTextFontSize
? qsTr("Are you sure you want to kick <b>%1</b> from %2?").arg(root.username).arg(root.communityName)
: qsTr("Are you sure you want to ban <b>%1</b> from %2? This means that they will be kicked from this community and banned from re-joining.").arg(root.username).arg(root.communityName)
}
StatusSwitch {
Layout.fillWidth: true
id: deleteAllMessagesSwitch
checked: false
}
visible: root.mode === KickBanPopup.Mode.Ban
leftSide: false
text: qsTr("Delete all messages posted by the user")
}
}
@ -74,8 +60,6 @@ StatusDialog {
onClicked: root.close()
}
StatusButton {
id: banButton
objectName: root.mode === KickBanPopup.Mode.Kick
? "CommunityMembers_KickModal_KickButton"
: "CommunityMembers_BanModal_BanButton"

View File

@ -55,8 +55,8 @@ StatusSectionLayout {
property var mutualContactsModel
property var blockedContactsModel
property var pendingReceivedRequestContactsModel
property var pendingSentRequestContactsModel
property var pendingContactsModel
property int pendingReceivedContactsCount
required property bool isCentralizedMetricsEnabled
@ -116,7 +116,7 @@ StatusSectionLayout {
syncingBadgeCount: root.store.devicesStore.devicesModel.count -
root.store.devicesStore.devicesModel.pairedCount
messagingBadgeCount: root.pendingReceivedRequestContactsModel.ModelCount.count
messagingBadgeCount: root.pendingReceivedContactsCount
}
headerBackground: AccountHeaderGradient {
@ -244,8 +244,8 @@ StatusSectionLayout {
mutualContactsModel: root.mutualContactsModel
blockedContactsModel: root.blockedContactsModel
pendingReceivedRequestContactsModel: root.pendingReceivedRequestContactsModel
pendingSentRequestContactsModel: root.pendingSentRequestContactsModel
pendingContactsModel: root.pendingContactsModel
pendingReceivedContactsCount: root.pendingReceivedContactsCount
}
}
@ -280,7 +280,7 @@ StatusSectionLayout {
contentWidth: d.contentWidth
sectionTitle: settingsEntriesModel.getNameForSubsection(Constants.settingsSubsection.messaging)
requestsCount: root.pendingReceivedRequestContactsModel.ModelCount.count
requestsCount: root.pendingReceivedContactsCount
messagingStore: root.store.messagingStore
}
}

View File

@ -1,49 +1,26 @@
import QtQuick 2.15
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
import shared.controls.delegates 1.0
StatusListItem {
ContactListItemDelegate {
id: root
width: parent.width
height: visible ? implicitHeight : 0
title: root.name
property string name
property string iconSource
property color pubKeyColor
property var colorHash
property bool showSendMessageButton: false
property bool showRejectContactRequestButton: false
property bool showAcceptContactRequestButton: false
property bool showRemoveRejectionButton: false
property string contactText: ""
signal contextMenuRequested
signal sendMessageRequested
signal showVerificationRequestRequested
signal acceptContactRequested
signal rejectRequestRequested
signal removeRejectionRequested
asset.width: 40
asset.height: 40
asset.color: root.pubKeyColor
asset.letterSize: asset._twoLettersSize
asset.charactersLen: 2
asset.name: root.iconSource
asset.isLetterIdenticon: root.iconSource.toString() === ""
ringSettings {
ringSpecModel: root.colorHash
ringPxSize: Math.max(asset.width / 24.0)
}
icon.width: 40
icon.height: 40
components: [
StatusFlatRoundButton {
@ -53,6 +30,7 @@ StatusListItem {
height: visible ? 32 : 0
icon.name: "chat"
icon.color: Theme.palette.directColor1
tooltip.text: qsTr("Send message")
onClicked: root.sendMessageRequested()
},
StatusFlatRoundButton {
@ -62,6 +40,7 @@ StatusListItem {
height: visible ? 32 : 0
icon.name: "close-circle"
icon.color: Theme.palette.dangerColor1
tooltip.text: qsTr("Reject")
onClicked: root.rejectRequestRequested()
},
StatusFlatRoundButton {
@ -71,26 +50,16 @@ StatusListItem {
height: visible ? 32 : 0
icon.name: "checkmark-circle"
icon.color: Theme.palette.successColor1
tooltip.text: qsTr("Accept")
onClicked: root.acceptContactRequested()
},
StatusFlatRoundButton {
objectName: "removeRejectBtn"
visible: showRemoveRejectionButton
width: visible ? 32 : 0
height: visible ? 32 : 0
icon.name: "cancel"
icon.color: Theme.palette.dangerColor1
onClicked: root.removeRejectionRequested()
},
StatusBaseText {
text: root.contactText
anchors.verticalCenter: parent.verticalCenter
color: Theme.palette.baseColor1
},
StatusFlatRoundButton {
objectName: "moreBtn"
id: menuButton
width: 32
height: 32
icon.name: "more"

View File

@ -1,152 +1,71 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import StatusQ 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import utils 1.0
import SortFilterProxyModel 0.2
Item {
StatusListView {
id: root
implicitHeight: (title.height + contactsList.height)
property var contactsModel
required property var contactsModel
property int panelUsage: Constants.contactsPanelUsage.unknownPosition
property string title: ""
property string searchString: ""
readonly property int count: contactsList.count
signal openContactContextMenu(string publicKey)
signal sendMessageActionTriggered(string publicKey)
signal showVerificationRequest(string publicKey)
signal contactRequestAccepted(string publicKey)
signal contactRequestRejected(string publicKey)
signal rejectionRemoved(string publicKey)
StyledText {
id: title
height: visible ? contentHeight : 0
anchors.left: parent.left
anchors.leftMargin: Theme.padding
visible: contactsList.count > 0 && root.title !== ""
text: root.title
font.weight: Font.Medium
font.pixelSize: 15
color: Theme.palette.secondaryText
}
StatusListView {
id: contactsList
objectName: "ContactListPanel_ListView"
anchors.top: title.bottom
anchors.left: parent.left
anchors.right: parent.right
onCountChanged: {
height = (count*64);
}
interactive: false
model: SortFilterProxyModel {
id: filteredModel
sourceModel: root.contactsModel
function panelUsagePredicate(isVerified) {
if (panelUsage === Constants.contactsPanelUsage.verifiedMutualContacts)
return isVerified
if (panelUsage === Constants.contactsPanelUsage.mutualContacts)
return !isVerified
return true
}
function searchPredicate(name, pubkey, compressedPubKey) {
const lowerCaseSearchString = root.searchString.toLowerCase()
return name.toLowerCase().includes(lowerCaseSearchString) ||
pubkey.toLowerCase().includes(lowerCaseSearchString) ||
compressedPubKey.toLowerCase().includes(lowerCaseSearchString)
}
filters: [
FastExpressionFilter {
expression: filteredModel.panelUsagePredicate(model.isVerified)
expectedRoles: ["isVerified"]
},
FastExpressionFilter {
enabled: root.searchString !== ""
expression: {
root.searchString // ensure expression is reevaluated when searchString changes
return filteredModel.searchPredicate(model.displayName, model.pubKey, model.compressedPubKey)
}
expectedRoles: ["displayName", "pubKey", "compressedPubKey"]
UserSearchFilterContainer {
searchString: root.searchString
}
]
sorters: StringSorter {
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 {
id: panelDelegate
width: ListView.view.width
name: model.preferredDisplayName
iconSource: model.thumbnailImage
subTitle: model.ensVerified ? "" : Utils.getElidedCompressedPk(model.pubKey)
pubKeyColor: Utils.colorForPubkey(model.pubKey)
colorHash: Utils.getColorHashAsJson(model.pubKey, model.ensVerified)
showSendMessageButton: model.isContact && !model.isBlocked
showRejectContactRequestButton: {
if (root.panelUsage === Constants.contactsPanelUsage.receivedContactRequest
&& !model.verificationRequestStatus)
return true
return false
}
showAcceptContactRequestButton: {
if (root.panelUsage === Constants.contactsPanelUsage.receivedContactRequest
&& !model.verificationRequestStatus)
return true
return false
}
showRemoveRejectionButton: {
if (root.panelUsage === Constants.contactsPanelUsage.rejectedReceivedContactRequest)
return true
return false
}
contactText: {
if (root.panelUsage === Constants.contactsPanelUsage.sentContactRequest)
return qsTr("Contact Request Sent")
if (root.panelUsage === Constants.contactsPanelUsage.rejectedSentContactRequest)
return qsTr("Contact Request Rejected")
return ""
}
showRejectContactRequestButton: root.panelUsage === Constants.contactsPanelUsage.pendingContacts &&
model.contactRequest === Constants.ContactRequestState.Received
showAcceptContactRequestButton: showRejectContactRequestButton
contactText: root.panelUsage === Constants.contactsPanelUsage.pendingContacts &&
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)
onRemoveRejectionRequested: root.rejectionRemoved(model.pubKey)
onShowVerificationRequestRequested: root.showVerificationRequest(model.pubKey)
}
}
}

View File

@ -1,4 +1,5 @@
ContactPanel 1.0 ContactPanel.qml
ContactsListPanel 1.0 ContactsListPanel.qml
ProfileDescriptionPanel 1.0 ProfileDescriptionPanel.qml
ProfileShowcaseAccountsPanel 1.0 ProfileShowcaseAccountsPanel.qml
ProfileShowcaseAssetsPanel 1.0 ProfileShowcaseAssetsPanel.qml

View File

@ -12,7 +12,7 @@ import StatusQ.Core.Backpressure 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import "../stores"
import AppLayouts.Profile.stores 1.0
StatusModal {
id: root

View File

@ -10,3 +10,4 @@ TokenListPopup 1.0 TokenListPopup.qml
WalletKeypairAccountMenu 1.0 WalletKeypairAccountMenu.qml
WalletAddressMenu 1.0 WalletAddressMenu.qml
ConfirmChangePasswordModal 1.0 ConfirmChangePasswordModal.qml
SendContactRequestModal 1.0 SendContactRequestModal.qml

View File

@ -18,9 +18,9 @@ import shared.stores 1.0 as SharedStores
import shared.views 1.0
import shared.views.chat 1.0
import "../stores"
import "../panels"
import "../popups"
import AppLayouts.Profile.stores 1.0
import AppLayouts.Profile.panels 1.0
import AppLayouts.Profile.popups 1.0
SettingsContentBase {
id: root
@ -28,20 +28,17 @@ SettingsContentBase {
property ContactsStore contactsStore
property SharedStores.UtilsStore utilsStore
property var mutualContactsModel
property var blockedContactsModel
property var pendingReceivedRequestContactsModel
property var pendingSentRequestContactsModel
required property var mutualContactsModel
required property var blockedContactsModel
required property var pendingContactsModel
required property int pendingReceivedContactsCount
property alias searchStr: searchBox.text
property bool isPending: false
titleRowComponentLoader.sourceComponent: StatusButton {
objectName: "ContactsView_ContactRequest_Button"
text: qsTr("Send contact request to chat key")
onClicked: {
Global.openPopup(sendContactRequest);
}
onClicked: sendContactRequestComponent.createObject(root).open()
}
function openContextMenu(model, pubKey) {
@ -67,11 +64,108 @@ SettingsContentBase {
Global.openMenu(contactContextMenuComponent, this, params)
}
Item {
id: contentItem
headerComponents: ColumnLayout {
width: root.contentWidth
height: (searchBox.height + contactsTabBar.height
+ stackLayout.height + (2 * Theme.bigPadding))
spacing: Theme.padding
StatusTabBar {
id: contactsTabBar
Layout.fillWidth: true
StatusTabButton {
readonly property int panelUsage: Constants.contactsPanelUsage.mutualContacts
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
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
text: qsTr("Blocked")
}
}
SearchBox {
id: searchBox
Layout.fillWidth: true
placeholderText: qsTr("Search by name or chat key")
}
}
ContactsListPanel {
id: contactsListPanel
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
}
}
section.property: {
switch (contactsListPanel.panelUsage) {
case Constants.contactsPanelUsage.pendingContacts:
return "contactRequest"
case Constants.contactsPanelUsage.mutualContacts:
return "isVerified"
case Constants.contactsPanelUsage.blockedContacts:
default:
return ""
}
}
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 ""
}
}
}
section.labelPositioning: ViewSection.InlineLabels | ViewSection.CurrentLabelAtStart
header: NoFriendsRectangle {
width: ListView.view.width
visible: ListView.view.count === 0
inviteButtonVisible: searchBox.text === ""
}
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
@ -98,191 +192,27 @@ SettingsContentBase {
onClosed: destroy()
}
}
SearchBox {
id: searchBox
anchors.left: parent.left
anchors.right: parent.right
placeholderText: qsTr("Search by a display name or chat key")
}
StatusTabBar {
id: contactsTabBar
anchors.left: parent.left
anchors.right: parent.right
anchors.top: searchBox.bottom
anchors.topMargin: Theme.padding
component SectionComponent: Rectangle {
required property string section
property alias text: sectionText.text
StatusTabButton {
id: contactsBtn
leftPadding: Theme.padding
width: implicitWidth
text: qsTr("Contacts")
}
StatusTabButton {
id: pendingRequestsBtn
objectName: "ContactsView_PendingRequest_Button"
width: implicitWidth
enabled: !root.pendingReceivedRequestContactsModel.ModelCount.empty ||
!root.pendingSentRequestContactsModel.ModelCount.empty
text: qsTr("Pending Requests")
badge.value: root.pendingReceivedRequestContactsModel.ModelCount.count
}
StatusTabButton {
id: blockedBtn
objectName: "ContactsView_Blocked_Button"
width: implicitWidth
enabled: !root.blockedContactsModel.ModelCount.empty
text: qsTr("Blocked")
}
}
width: ListView.view.width
height: sectionText.implicitHeight
color: Theme.palette.statusListItem.backgroundColor
StackLayout {
id: stackLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.top: contactsTabBar.bottom
currentIndex: contactsTabBar.currentIndex
anchors.topMargin: Theme.padding
// CONTACTS
ColumnLayout {
Layout.fillWidth: true
Layout.minimumHeight: 0
Layout.maximumHeight: (verifiedContacts.height + mutualContacts.height + noFriendsItem.height)
visible: (stackLayout.currentIndex === 0)
onVisibleChanged: {
if (visible) {
stackLayout.height = height+contactsTabBar.anchors.topMargin;
}
}
spacing: Theme.padding
ContactsListPanel {
id: verifiedContacts
StatusBaseText {
id: sectionText
width: parent.width
anchors.verticalCenter: parent.verticalCenter
topPadding: Theme.halfPadding
bottomPadding: Theme.halfPadding
Layout.fillWidth: true
title: qsTr("Trusted Contacts")
visible: !noFriendsItem.visible && count > 0
contactsModel: root.mutualContactsModel
searchString: searchBox.text
onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey)
panelUsage: Constants.contactsPanelUsage.verifiedMutualContacts
onSendMessageActionTriggered: {
root.contactsStore.joinPrivateChat(publicKey)
}
}
ContactsListPanel {
id: mutualContacts
Layout.fillWidth: true
visible: !noFriendsItem.visible && count > 0
title: qsTr("Contacts")
contactsModel: root.mutualContactsModel
searchString: searchBox.text
onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey)
panelUsage: Constants.contactsPanelUsage.mutualContacts
onSendMessageActionTriggered: {
root.contactsStore.joinPrivateChat(publicKey)
}
}
Item {
id: noFriendsItem
Layout.fillWidth: true
Layout.preferredHeight: visible ? (root.contentHeight - (2*searchBox.height) - contactsTabBar.height - contactsTabBar.anchors.topMargin) : 0
visible: root.mutualContactsModel.ModelCount.empty
NoFriendsRectangle {
anchors.centerIn: parent
text: qsTr("You don't have any contacts yet")
}
}
}
// PENDING REQUESTS
ColumnLayout {
Layout.fillWidth: true
Layout.minimumHeight: 0
Layout.maximumHeight: (receivedRequests.height + sentRequests.height)
spacing: Theme.padding
visible: (stackLayout.currentIndex === 1)
onVisibleChanged: {
if (visible) {
stackLayout.height = height+contactsTabBar.anchors.topMargin;
}
}
ContactsListPanel {
id: receivedRequests
objectName: "receivedRequests_ContactsListPanel"
Layout.fillWidth: true
title: qsTr("Received")
searchString: searchBox.text
visible: count > 0
onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey)
contactsModel: root.pendingReceivedRequestContactsModel
panelUsage: Constants.contactsPanelUsage.receivedContactRequest
onSendMessageActionTriggered: {
root.contactsStore.joinPrivateChat(publicKey)
}
onContactRequestAccepted: {
root.contactsStore.acceptContactRequest(publicKey, "")
}
onContactRequestRejected: {
root.contactsStore.dismissContactRequest(publicKey, "")
}
}
ContactsListPanel {
id: sentRequests
objectName: "sentRequests_ContactsListPanel"
Layout.fillWidth: true
title: qsTr("Sent")
searchString: searchBox.text
visible: count > 0
onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey)
contactsModel: root.pendingSentRequestContactsModel
panelUsage: Constants.contactsPanelUsage.sentContactRequest
}
}
// BLOCKED
ContactsListPanel {
id: blockedContacts
Layout.fillWidth: true
searchString: searchBox.text
onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey)
contactsModel: root.blockedContactsModel
panelUsage: Constants.contactsPanelUsage.blockedContacts
visible: (stackLayout.currentIndex === 2)
onVisibleChanged: {
if (visible) {
stackLayout.height = height;
}
}
}
}
Component {
id: loadingIndicator
StatusLoadingIndicator {
width: 12
height: 12
}
}
Component {
id: sendContactRequest
SendContactRequestModal {
contactsStore: root.contactsStore
onClosed: destroy()
}
color: Theme.palette.baseColor1
font.pixelSize: Theme.additionalTextSize
font.weight: Font.Medium
elide: Text.ElideRight
}
}
}

View File

@ -2,8 +2,10 @@ AboutView 1.0 AboutView.qml
AppearanceView 1.0 AppearanceView.qml
ChangePasswordView 1.0 ChangePasswordView.qml
CommunitiesView 1.0 CommunitiesView.qml
ContactsView 1.0 ContactsView.qml
CurrenciesModel 1.0 CurrenciesModel.qml
LanguageView 1.0 LanguageView.qml
NotificationsView 1.0 NotificationsView.qml
PrivacyAndSecurityView 1.0 PrivacyAndSecurityView.qml
SyncingView 1.0 SyncingView.qml
SettingsContentBase 1.0 SettingsContentBase.qml

View File

@ -541,7 +541,7 @@ Item {
Global.displayToastMessage(toastTitle, toastSubtitle, toastIcon, toastLoading, toastType, toastLink)
}
function onCommunityMemberStatusEphemeralNotification(communityName: string, memberName: string, state: CommunityMembershipRequestState) {
function onCommunityMemberStatusEphemeralNotification(communityName: string, memberName: string, state: int) {
var text = ""
switch (state) {
case Constants.CommunityMembershipRequestState.Banned:
@ -1746,8 +1746,8 @@ Item {
mutualContactsModel: contactsModelAdaptor.mutualContacts
blockedContactsModel: contactsModelAdaptor.blockedContacts
pendingReceivedRequestContactsModel: contactsModelAdaptor.pendingReceivedRequestContacts
pendingSentRequestContactsModel: contactsModelAdaptor.pendingSentRequestContacts
pendingContactsModel: contactsModelAdaptor.pendingContacts
pendingReceivedContactsCount: contactsModelAdaptor.pendingReceivedRequestContacts.count
Binding on settingsSubsection {
value: profileLoader.settingsSubsection

View File

@ -19,7 +19,7 @@ QObject {
localNickname [string] - local nickname set by the current user
alias [string] - generated 3 word name
icon [string] - thumbnail image of the user
colorId [string] - generated color ID for the user's profile
colorId [int] - generated color ID for the user's profile
colorHash [string] - generated color hash for the user's profile
onlineStatus [int] - the online status of the member
isContact [bool] - whether the user is a mutual contact or not
@ -75,4 +75,21 @@ QObject {
value: Constants.ContactRequestState.Sent
}
}
readonly property var pendingContacts: SortFilterProxyModel {
sourceModel: root.allContacts ?? null
filters: [
AnyOf {
ValueFilter {
roleName: "contactRequest"
value: Constants.ContactRequestState.Received
}
ValueFilter {
roleName: "contactRequest"
value: Constants.ContactRequestState.Sent
}
}
]
}
}

View File

@ -0,0 +1,39 @@
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

@ -18,10 +18,11 @@ StatusMemberListItem {
pubKey: model.isEnsVerified ? "" : model.compressedPubKey
nickName: model.localNickname
userName: ProfileUtils.displayName("", model.ensName, model.displayName, model.alias)
isVerified: model.isVerified
isUntrustworthy: model.isUntrustworthy
isBlocked: model.isBlocked
isVerified: model.isVerified || model.trustStatus === Constants.trustStatus.trusted
isUntrustworthy: model.isUntrustworthy || model.trustStatus === Constants.trustStatus.untrustworthy
isContact: model.isContact
icon.name: model.icon
icon.name: model.thumbnailImage || model.icon
icon.color: Utils.colorForColorId(model.colorId)
status: model.onlineStatus
ringSettings.ringSpecModel: model.colorHash

View File

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

View File

@ -1,31 +1,31 @@
import QtQuick 2.15
import utils 1.0
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import "../popups"
import utils 1.0
import shared.popups 1.0
Item {
id: noContactsRect
id: root
implicitWidth: 260
implicitHeight: visible ? 120 : 0
property string text: qsTr("You dont have any contacts yet. Invite your friends to start chatting.")
property string text: inviteButtonVisible ? qsTr("You dont have any contacts yet. Invite your friends to start chatting.")
: qsTr("No users match your search")
property alias textColor: noContacts.color
property bool inviteButtonVisible: true
StatusBaseText {
id: noContacts
text: noContactsRect.text
text: root.text
color: Theme.palette.baseColor1
anchors.top: parent.top
anchors.topMargin: Theme.padding
anchors.left: parent.left
anchors.right: parent.right
wrapMode: Text.WordWrap
font.pixelSize: 15
horizontalAlignment: Text.AlignHCenter
}
StatusButton {
@ -33,7 +33,8 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: noContacts.bottom
anchors.topMargin: Theme.padding
onClicked: Global.openPopup(inviteFriendsPopup);
visible: root.inviteButtonVisible
onClicked: inviteFriendsPopup.createObject(root).open()
}
Component {

View File

@ -498,12 +498,8 @@ QtObject {
readonly property QtObject contactsPanelUsage: QtObject {
readonly property int unknownPosition: -1
readonly property int mutualContacts: 0
readonly property int verifiedMutualContacts: 1
readonly property int sentContactRequest: 2
readonly property int receivedContactRequest: 3
readonly property int rejectedSentContactRequest: 4
readonly property int rejectedReceivedContactRequest: 5
readonly property int blockedContacts: 6
readonly property int pendingContacts: 1
readonly property int blockedContacts: 2
}
readonly property QtObject keypair: QtObject {