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.Chat.stores 1.0 as ChatStores
import AppLayouts.Profile.stores 1.0 as ProfileStores import AppLayouts.Profile.stores 1.0 as ProfileStores
import shared.stores 1.0
import utils 1.0 import utils 1.0
import Models 1.0 import Models 1.0
@ -15,8 +16,6 @@ import SortFilterProxyModel 0.2
import Storybook 1.0 import Storybook 1.0
import StatusQ 0.1 import StatusQ 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
SplitView { SplitView {
id: root id: root
@ -24,46 +23,27 @@ SplitView {
orientation: Qt.Vertical orientation: Qt.Vertical
Logs { id: logs } 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 { MembersTabPanel {
id: membersTabPanelPage id: membersTabPanelPage
SplitView.fillWidth: true SplitView.fillWidth: true
SplitView.fillHeight: true SplitView.fillHeight: true
placeholderText: "Search users"
model: usersModelWithMembershipState model: usersModelWithMembershipState
panelType: viewStateSelector.currentValue panelType: viewStateSelector.currentValue
searchString: ctrlSearch.text
rootStore: ChatStores.RootStore { rootStore: ChatStores.RootStore {
contactsStore: ProfileStores.ContactsStore { contactsStore: ProfileStores.ContactsStore {
readonly property string myPublicKey: "0x000" readonly property string myPublicKey: "0x000"
} }
} }
utilsStore: UtilsStore {
function getEmojiHash(publicKey) {
if (publicKey === "")
return ""
return JSON.stringify(["👨🏻‍🍼", "🏃🏿‍♂️", "🌇", "🤶🏿", "🏮","🤷🏻‍♂️", "🤦🏻", "📣", "🤎", "👷🏽", "😺", "🥞", "🔃", "🧝🏽‍♂️"])
}
}
onKickUserClicked: { onKickUserClicked: {
logs.logEvent("MembersTabPanel::onKickUserClicked", ["id", "name"], arguments) logs.logEvent("MembersTabPanel::onKickUserClicked", ["id", "name"], arguments)
@ -132,7 +112,7 @@ SplitView {
} }
LogsAndControlsPanel { LogsAndControlsPanel {
SplitView.minimumHeight: 100 SplitView.minimumHeight: 200
SplitView.preferredHeight: 320 SplitView.preferredHeight: 320
logsView.logText: logs.logText logsView.logText: logs.logText
@ -144,6 +124,7 @@ SplitView {
} }
ComboBox { ComboBox {
Layout.preferredWidth: 300
id: viewStateSelector id: viewStateSelector
textRole: "text" textRole: "text"
valueRole: "value" valueRole: "value"
@ -155,6 +136,13 @@ SplitView {
ListElement { text: "Declined Members"; value: MembersTabPanel.TabType.DeclinedRequests } 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 // 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 { StatusTabButton {
width: implicitWidth width: implicitWidth
enabled: false enabled: false
text: qsTr("Blocked & disabled") text: "Blocked & disabled"
} }
StatusTabButton { StatusTabButton {
width: implicitWidth width: implicitWidth

View File

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

View File

@ -1,10 +1,5 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core.Utils 0.1
import utils 1.0
Flow { Flow {
id: root 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_Item = {"container": mainWindow_ContactsView, "type": "Item", "unnamed": 1, "visible": True}
settingsContentBaseScrollView_sentRequests_ContactsListPanel = {"container": mainWindow_ContactsView, "objectName": "sentRequests_ContactsListPanel", "type": "ContactsListPanel", "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} 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_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_Invite_friends_StatusButton = {"container": mainWindow_ContactsView, "type": "StatusButton", "unnamed": 1, "visible": True}
settingsContentBaseScrollView_NoFriendsRectangle = {"container": mainWindow_ContactsView, "type": "NoFriendsRectangle", "unnamed": 1, "visible": True} settingsContentBaseScrollView_NoFriendsRectangle = {"container": mainWindow_ContactsView, "type": "NoFriendsRectangle", "unnamed": 1, "visible": True}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,25 +10,27 @@ import StatusQ.Controls 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import StatusQ.Popups 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.chat 1.0
import shared.controls.delegates 1.0
import shared.stores 1.0 as SharedStores import shared.stores 1.0 as SharedStores
import shared.views.chat 1.0 import shared.views.chat 1.0
import utils 1.0 import utils 1.0
import AppLayouts.Chat.stores 1.0 import AppLayouts.Chat.stores 1.0
import AppLayouts.Communities.layouts 1.0
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
Item { Item {
id: root id: root
property string placeholderText: qsTr("Search by member name or chat key") required property var model
property var model
property string searchString
property RootStore rootStore property RootStore rootStore
property SharedStores.UtilsStore utilsStore property SharedStores.UtilsStore utilsStore
property int panelType: MembersTabPanel.TabType.AllMembers
property int memberRole: Constants.memberRole.none property int memberRole: Constants.memberRole.none
readonly property bool isOwner: memberRole === Constants.memberRole.owner readonly property bool isOwner: memberRole === Constants.memberRole.owner
@ -49,332 +51,279 @@ Item {
DeclinedRequests DeclinedRequests
} }
property int panelType: MembersTabPanel.TabType.AllMembers StatusListView {
objectName: "CommunityMembersTabPanel_MembersListViews"
ColumnLayout {
anchors.fill: parent anchors.fill: parent
spacing: 30
SearchBox { model: SortFilterProxyModel {
id: memberSearch sourceModel: root.model
Layout.preferredWidth: 400
Layout.leftMargin: 12 sorters: [
placeholderText: root.placeholderText StringSorter {
enabled: !!root.model && !root.model.ModelCount.empty roleName: "preferredDisplayName"
caseSensitivity: Qt.CaseInsensitive
}
]
filters: [
UserSearchFilterContainer {
searchString: root.searchString
}
]
} }
StatusListView { spacing: 0
id: membersList
objectName: "CommunityMembersTabPanel_MembersListViews"
Layout.fillWidth: true delegate: ContactListItemDelegate {
Layout.fillHeight: true id: memberItem
model: SortFilterProxyModel { // Buttons visibility conditions:
id: filteredModel // 1. Tab based buttons - only visible when the tab is selected
sourceModel: root.model // a. All members tab
// - Kick; - Kick pending
// - Ban; - Ban pending
// b. Pending requests tab
// - Accept; - Accept pending
// - Reject; - Reject pending
// c. Rejected members tab
// - Accept; - Accept pending
// d. Banned members tab
// - Unban
// 2. Pending states - buttons in pending states are always visible in their specific tab. Other buttons are disabled if the request is in pending state
// - Accept button is visible when the user is hovered or when the request is in accepted pending state. This condition can be overriden by the ctaAllowed property
// - Reject button is visible when the user is hovered or when the request is in rejected pending state. This condition can be overriden by the ctaAllowed property
// - Kick and ban buttons are visible when the user is hovered or when the request is in kick or ban pending state. This condition can be overriden by the ctaAllowed property
// 3. Other conditions - buttons are visible when the user is hovered and is not himself or other privileged user
// 4. All members tab, member in AwaitingAddress state - buttons is not visible, sandwatch icon is shown
function searchPredicate(ensName, displayName, aliasName) { /// Helpers ///
const lowerCaseSearchString = memberSearch.text.toLowerCase()
const secondaryName = ProfileUtils.displayName("", ensName, displayName, aliasName)
return secondaryName.toLowerCase().includes(lowerCaseSearchString) // Tab based buttons
} readonly property bool tabIsShowingKickBanButtons: root.panelType === MembersTabPanel.TabType.AllMembers
readonly property bool tabIsShowingUnbanButton: root.panelType === MembersTabPanel.TabType.BannedMembers
readonly property bool tabIsShowingRejectButton: root.panelType === MembersTabPanel.TabType.PendingRequests
readonly property bool tabIsShowingAcceptButton: root.panelType === MembersTabPanel.TabType.PendingRequests ||
root.panelType === MembersTabPanel.TabType.DeclinedRequests
readonly property bool tabIsShowingViewMessagesButton: model.membershipRequestState !== Constants.CommunityMembershipRequestState.BannedWithAllMessagesDelete &&
(root.panelType === MembersTabPanel.TabType.AllMembers ||
root.panelType === MembersTabPanel.TabType.BannedMembers)
sorters : [
StringSorter {
roleName: "preferredDisplayName"
caseSensitivity: Qt.CaseInsensitive
}
]
filters: AnyOf { // Request states
enabled: memberSearch.text !== "" readonly property bool isPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.Pending
// substring search for either nickname or the other primary/secondary display name readonly property bool isAccepted: model.membershipRequestState === Constants.CommunityMembershipRequestState.Accepted
SearchFilter { readonly property bool isRejected: model.membershipRequestState === Constants.CommunityMembershipRequestState.Rejected
roleName: "localNickname" readonly property bool isRejectedPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.RejectedPending
searchPhrase: memberSearch.text readonly property bool isAcceptedPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.AcceptedPending
} readonly property bool isBanPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.BannedPending
FastExpressionFilter { readonly property bool isUnbanPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.UnbannedPending
expression: { readonly property bool isKickPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.KickedPending
memberSearch.text readonly property bool isBanned: model.membershipRequestState === Constants.CommunityMembershipRequestState.Banned ||
return filteredModel.searchPredicate(model.ensName, model.displayName, model.alias) model.membershipRequestState === Constants.CommunityMembershipRequestState.BannedWithAllMessagesDelete
} readonly property bool isKicked: model.membershipRequestState === Constants.CommunityMembershipRequestState.Kicked
expectedRoles: ["ensName", "displayName", "alias"]
} // TODO: Connect to backend when available
// exact search for the full key // The admin that initited the pending state can change the state. Actions are not visible for other admins
ValueFilter { readonly property bool ctaAllowed: !isRejectedPending && !isAcceptedPending && !isBanPending && !isUnbanPending && !isKickPending
roleName: "compressedPubKey"
value: memberSearch.text readonly property bool canBeBanned: {
} if (model.isCurrentUser)
return false
switch (model.memberRole) {
// Owner can't be banned
case Constants.memberRole.owner: return false
// TokenMaster can only be banned by owner
case Constants.memberRole.tokenMaster: return root.isOwner
// Admin can only be banned by owner and tokenMaster
case Constants.memberRole.admin: return root.isOwner || root.isTokenMaster
// All normal members can be banned by all privileged users
default: return true
} }
} }
spacing: 0 readonly property bool showOnHover: hovered && ctaAllowed
readonly property bool canDeleteMessages: model.isCurrentUser || model.memberRole !== Constants.memberRole.owner
delegate: StatusMemberListItem { /// Button visibility ///
id: memberItem readonly property bool acceptButtonVisible: tabIsShowingAcceptButton && (isPending || isRejected || isRejectedPending || isAcceptedPending) && showOnHover
readonly property bool rejectButtonVisible: tabIsShowingRejectButton && (isPending || isRejectedPending || isAcceptedPending) && showOnHover
readonly property bool acceptPendingButtonVisible: tabIsShowingAcceptButton && isAcceptedPending
readonly property bool rejectPendingButtonVisible: tabIsShowingRejectButton && isRejectedPending
readonly property bool kickButtonVisible: tabIsShowingKickBanButtons && isAccepted && showOnHover && canBeBanned
readonly property bool banButtonVisible: tabIsShowingKickBanButtons && isAccepted && showOnHover && canBeBanned
readonly property bool kickPendingButtonVisible: tabIsShowingKickBanButtons && isKickPending
readonly property bool banPendingButtonVisible: tabIsShowingKickBanButtons && isBanPending
readonly property bool unbanButtonVisible: tabIsShowingUnbanButton && isBanned && showOnHover
readonly property bool viewMessagesButtonVisible: tabIsShowingViewMessagesButton && showOnHover
readonly property bool messagesDeletedTextVisible: showOnHover &&
model.membershipRequestState === Constants.CommunityMembershipRequestState.BannedWithAllMessagesDelete
// Buttons visibility conditions: /// Pending states ///
// 1. Tab based buttons - only visible when the tab is selected readonly property bool isPendingState: isAcceptedPending || isRejectedPending || isBanPending || isUnbanPending || isKickPending
// a. All members tab readonly property string pendingStateText: isAcceptedPending ? qsTr("Accept pending...") :
// - Kick; - Kick pending isRejectedPending ? qsTr("Reject pending...") :
// - Ban; - Ban pending isBanPending ? qsTr("Ban pending...") :
// b. Pending requests tab isUnbanPending ? qsTr("Unban pending...") :
// - Accept; - Accept pending isKickPending ? qsTr("Kick pending...") : ""
// - Reject; - Reject pending
// c. Rejected members tab
// - Accept; - Accept pending
// d. Banned members tab
// - Unban
// 2. Pending states - buttons in pending states are always visible in their specific tab. Other buttons are disabled if the request is in pending state
// - Accept button is visible when the user is hovered or when the request is in accepted pending state. This condition can be overriden by the ctaAllowed property
// - Reject button is visible when the user is hovered or when the request is in rejected pending state. This condition can be overriden by the ctaAllowed property
// - Kick and ban buttons are visible when the user is hovered or when the request is in kick or ban pending state. This condition can be overriden by the ctaAllowed property
// 3. Other conditions - buttons are visible when the user is hovered and is not himself or other privileged user
// 4. All members tab, member in AwaitingAddress state - buttons is not visible, sandwatch icon is shown
/// Helpers /// isAwaitingAddress: model.membershipRequestState === Constants.CommunityMembershipRequestState.AwaitingAddress
// Tab based buttons components: [
readonly property bool tabIsShowingKickBanButtons: root.panelType === MembersTabPanel.TabType.AllMembers StatusBaseText {
readonly property bool tabIsShowingUnbanButton: root.panelType === MembersTabPanel.TabType.BannedMembers id: pendingText
readonly property bool tabIsShowingRejectButton: root.panelType === MembersTabPanel.TabType.PendingRequests width: Math.max(implicitWidth, d.pendingTextMaxWidth)
readonly property bool tabIsShowingAcceptButton: root.panelType === MembersTabPanel.TabType.PendingRequests || onImplicitWidthChanged: {
root.panelType === MembersTabPanel.TabType.DeclinedRequests d.pendingTextMaxWidth = Math.max(implicitWidth, d.pendingTextMaxWidth)
readonly property bool tabIsShowingViewMessagesButton: model.membershipRequestState !== Constants.CommunityMembershipRequestState.BannedWithAllMessagesDelete &&
(root.panelType === MembersTabPanel.TabType.AllMembers ||
root.panelType === MembersTabPanel.TabType.BannedMembers)
// Request states
readonly property bool isPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.Pending
readonly property bool isAccepted: model.membershipRequestState === Constants.CommunityMembershipRequestState.Accepted
readonly property bool isRejected: model.membershipRequestState === Constants.CommunityMembershipRequestState.Rejected
readonly property bool isRejectedPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.RejectedPending
readonly property bool isAcceptedPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.AcceptedPending
readonly property bool isBanPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.BannedPending
readonly property bool isUnbanPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.UnbannedPending
readonly property bool isKickPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.KickedPending
readonly property bool isBanned: model.membershipRequestState === Constants.CommunityMembershipRequestState.Banned ||
model.membershipRequestState === Constants.CommunityMembershipRequestState.BannedWithAllMessagesDelete
readonly property bool isKicked: model.membershipRequestState === Constants.CommunityMembershipRequestState.Kicked
// TODO: Connect to backend when available
// 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
switch (model.memberRole) {
// Owner can't be banned
case Constants.memberRole.owner: return false
// TokenMaster can only be banned by owner
case Constants.memberRole.tokenMaster: return root.isOwner
// Admin can only be banned by owner and tokenMaster
case Constants.memberRole.admin: return root.isOwner || root.isTokenMaster
// All normal members can be banned by all privileged users
default: return true
} }
visible: !!text && isPendingState
rightPadding: isKickPending || isBanPending || isUnbanPending ? 0 : Theme.bigPadding
anchors.verticalCenter: parent.verticalCenter
text: pendingStateText
color: Theme.palette.baseColor1
StatusToolTip {
text: qsTr("Waiting for owner node to come online")
visible: hoverHandler.hovered
}
HoverHandler {
id: hoverHandler
enabled: pendingText.visible
}
},
StatusBaseText {
text: qsTr("Messages deleted")
color: Theme.palette.baseColor1
anchors.verticalCenter: parent.verticalCenter
visible: messagesDeletedTextVisible
},
StatusButton {
id: viewMessages
anchors.verticalCenter: parent.verticalCenter
objectName: "MemberListItem_ViewMessages"
text: qsTr("View Messages")
visible: viewMessagesButtonVisible
size: StatusBaseButton.Size.Small
onClicked: root.viewMemberMessagesClicked(model.pubKey, memberItem.title)
},
StatusButton {
anchors.verticalCenter: parent.verticalCenter
objectName: "MemberListItem_KickButton"
text: qsTr("Kick")
visible: kickButtonVisible
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
onClicked: root.kickUserClicked(model.pubKey, memberItem.title)
},
StatusButton {
objectName: "MemberListItem_BanButton"
anchors.verticalCenter: parent.verticalCenter
visible: banButtonVisible
text: qsTr("Ban")
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
onClicked: root.banUserClicked(model.pubKey, memberItem.title)
},
StatusButton {
objectName: "MemberListItem_UnbanButton"
anchors.verticalCenter: parent.verticalCenter
visible: unbanButtonVisible
text: qsTr("Unban")
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
onClicked: root.unbanUserClicked(model.pubKey)
},
StatusButton {
id: acceptButton
anchors.verticalCenter: parent.verticalCenter
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
enabled: !acceptPendingButtonVisible
onClicked: root.acceptRequestToJoin(model.requestToJoinId)
},
StatusButton {
id: rejectButton
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
onClicked: root.declineRequestToJoin(model.requestToJoinId)
} }
readonly property bool showOnHover: isHovered && ctaAllowed ]
readonly property bool canDeleteMessages: model.isCurrentUser || model.memberRole !== Constants.memberRole.owner
/// Button visibility /// readonly property string title: model.preferredDisplayName
readonly property bool acceptButtonVisible: tabIsShowingAcceptButton && (isPending || isRejected || isRejectedPending || isAcceptedPending) && showOnHover
readonly property bool rejectButtonVisible: tabIsShowingRejectButton && (isPending || isRejectedPending || isAcceptedPending) && showOnHover
readonly property bool acceptPendingButtonVisible: tabIsShowingAcceptButton && isAcceptedPending
readonly property bool rejectPendingButtonVisible: tabIsShowingRejectButton && isRejectedPending
readonly property bool kickButtonVisible: tabIsShowingKickBanButtons && isAccepted && showOnHover && canBeBanned
readonly property bool banButtonVisible: tabIsShowingKickBanButtons && isAccepted && showOnHover && canBeBanned
readonly property bool kickPendingButtonVisible: tabIsShowingKickBanButtons && isKickPending
readonly property bool banPendingButtonVisible: tabIsShowingKickBanButtons && isBanPending
readonly property bool unbanButtonVisible: tabIsShowingUnbanButton && isBanned && showOnHover
readonly property bool viewMessagesButtonVisible: tabIsShowingViewMessagesButton && showOnHover
readonly property bool messagesDeletedTextVisible: showOnHover &&
model.membershipRequestState === Constants.CommunityMembershipRequestState.BannedWithAllMessagesDelete
/// Pending states /// width: ListView.view.width
readonly property bool isPendingState: isAcceptedPending || isRejectedPending || isBanPending || isUnbanPending || isKickPending
readonly property string pendingStateText: isAcceptedPending ? qsTr("Accept pending...") :
isRejectedPending ? qsTr("Reject pending...") :
isBanPending ? qsTr("Ban pending...") :
isUnbanPending ? qsTr("Unban pending...") :
isKickPending ? qsTr("Kick pending...") : ""
isAwaitingAddress: model.membershipRequestState === Constants.CommunityMembershipRequestState.AwaitingAddress icon.width: 40
icon.height: 40
rightPadding: 75 onClicked: {
leftPadding: 12 if (mouse.button === Qt.RightButton) {
const profileType = Utils.getProfileType(model.isCurrentUser, false, model.isBlocked)
const contactType = Utils.getContactType(model.contactRequest, model.isContact)
components: [ const params = {
StatusBaseText { profileType, contactType,
id: pendingText pubKey: model.pubKey,
width: Math.max(implicitWidth, d.pendingTextMaxWidth) compressedPubKey: model.compressedPubKey,
onImplicitWidthChanged: { emojiHash: root.utilsStore.getEmojiHash(model.pubKey),
d.pendingTextMaxWidth = Math.max(implicitWidth, d.pendingTextMaxWidth) colorHash: model.colorHash,
} colorId: model.colorId,
visible: !!text && isPendingState displayName: memberItem.title || model.displayName,
rightPadding: isKickPending || isBanPending || isUnbanPending ? 0 : Theme.bigPadding userIcon: model.icon,
anchors.verticalCenter: parent.verticalCenter trustStatus: model.trustStatus,
text: pendingStateText onlineStatus: model.onlineStatus,
color: Theme.palette.baseColor1 ensVerified: model.isEnsVerified,
StatusToolTip { hasLocalNickname: !!model.localNickname
text: qsTr("Waiting for owner node to come online")
visible: hoverHandler.hovered
}
HoverHandler {
id: hoverHandler
enabled: pendingText.visible
}
},
StatusBaseText {
text: qsTr("Messages deleted")
color: Theme.palette.baseColor1
anchors.verticalCenter: parent.verticalCenter
visible: messagesDeletedTextVisible
},
StatusButton {
id: viewMessages
anchors.verticalCenter: parent.verticalCenter
objectName: "MemberListItem_ViewMessages"
text: qsTr("View Messages")
visible: viewMessagesButtonVisible
size: StatusBaseButton.Size.Small
onClicked: root.viewMemberMessagesClicked(model.pubKey, memberItem.title)
},
StatusButton {
id: kickButton
anchors.verticalCenter: parent.verticalCenter
objectName: "MemberListItem_KickButton"
text: qsTr("Kick")
visible: kickButtonVisible
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
onClicked: root.kickUserClicked(model.pubKey, memberItem.title)
},
StatusButton {
id: banButton
objectName: "MemberListItem_BanButton"
anchors.verticalCenter: parent.verticalCenter
visible: banButtonVisible
text: qsTr("Ban")
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
onClicked: root.banUserClicked(model.pubKey, memberItem.title)
},
StatusButton {
objectName: "MemberListItem_UnbanButton"
anchors.verticalCenter: parent.verticalCenter
visible: unbanButtonVisible
text: qsTr("Unban")
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
onClicked: root.unbanUserClicked(model.pubKey)
},
StatusButton {
id: acceptButton
anchors.verticalCenter: parent.verticalCenter
opacity: acceptButtonVisible
text: qsTr("Accept")
type: StatusBaseButton.Type.Success
icon.name: "checkmark-circle"
icon.color: enabled ? Theme.palette.successColor1 : disabledTextColor
loading: model.requestToJoinLoading
enabled: !acceptPendingButtonVisible
onClicked: root.acceptRequestToJoin(model.requestToJoinId)
},
StatusButton {
id: rejectButton
opacity: rejectButtonVisible
text: qsTr("Reject")
type: StatusBaseButton.Type.Danger
icon.name: "close-circle"
icon.color: enabled ? Theme.palette.dangerColor1 : disabledTextColor
enabled: !rejectPendingButtonVisible
onClicked: root.declineRequestToJoin(model.requestToJoinId)
} }
]
readonly property string title: model.preferredDisplayName memberContextMenuComponent.createObject(root, params).popup(this)
} else if (mouse.button === Qt.LeftButton) {
width: membersList.width Global.openProfilePopup(model.pubKey)
color: "transparent"
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) {
const profileType = Utils.getProfileType(model.isCurrentUser, false, model.isBlocked)
const contactType = Utils.getContactType(model.contactRequest, model.isContact)
const params = {
profileType, contactType,
pubKey: model.pubKey,
compressedPubKey: model.compressedPubKey,
emojiHash: root.utilsStore.getEmojiHash(model.pubKey),
colorHash: model.colorHash,
colorId: model.colorId,
displayName: memberItem.title || model.displayName,
userIcon: model.icon,
trustStatus: model.trustStatus,
onlineStatus: model.onlineStatus,
ensVerified: model.isEnsVerified,
hasLocalNickname: !!model.localNickname
}
Global.openMenu(memberContextMenuComponent, this, params)
} else if (mouse.button === Qt.LeftButton) {
Global.openProfilePopup(model.pubKey)
}
} }
} }
} }
}
Component { Component {
id: memberContextMenuComponent id: memberContextMenuComponent
ProfileContextMenu { ProfileContextMenu {
id: memberContextMenuView id: memberContextMenuView
required property string pubKey required property string pubKey
onOpenProfileClicked: Global.openProfilePopup(pubKey, null) onOpenProfileClicked: Global.openProfilePopup(pubKey, null)
onCreateOneToOneChat: { onCreateOneToOneChat: {
Global.changeAppSectionBySectionType(Constants.appSection.chat) Global.changeAppSectionBySectionType(Constants.appSection.chat)
root.rootStore.chatCommunitySectionModule.createOneToOneChat("", pubKey, "") root.rootStore.chatCommunitySectionModule.createOneToOneChat("", pubKey, "")
}
onReviewContactRequest: Global.openReviewContactRequestPopup(pubKey, null)
onSendContactRequest: Global.openContactRequestPopup(pubKey, null)
onEditNickname: Global.openNicknamePopupRequested(pubKey, null)
onRemoveNickname: root.rootStore.contactsStore.changeContactNickname(pubKey, "", displayName, true)
onUnblockContact: Global.unblockContactRequested(pubKey)
onMarkAsUntrusted: Global.markAsUntrustedRequested(pubKey)
onRemoveTrustStatus: root.rootStore.contactsStore.removeTrustStatus(pubKey)
onRemoveContact: Global.removeContactRequested(pubKey)
onBlockContact: Global.blockContactRequested(pubKey)
onMarkAsTrusted: Global.openMarkAsIDVerifiedPopup(pubKey, null)
onRemoveTrustedMark: Global.openRemoveIDVerificationDialog(pubKey, null)
onClosed: destroy()
} }
onReviewContactRequest: Global.openReviewContactRequestPopup(pubKey, null)
onSendContactRequest: Global.openContactRequestPopup(pubKey, null)
onEditNickname: Global.openNicknamePopupRequested(pubKey, null)
onRemoveNickname: root.rootStore.contactsStore.changeContactNickname(pubKey, "", displayName, true)
onUnblockContact: Global.unblockContactRequested(pubKey)
onMarkAsUntrusted: Global.markAsUntrustedRequested(pubKey)
onRemoveTrustStatus: root.rootStore.contactsStore.removeTrustStatus(pubKey)
onRemoveContact: Global.removeContactRequested(pubKey)
onBlockContact: Global.blockContactRequested(pubKey)
onMarkAsTrusted: Global.openMarkAsIDVerifiedPopup(pubKey, null)
onRemoveTrustedMark: Global.openRemoveIDVerificationDialog(pubKey, null)
onClosed: destroy()
} }
} }
@ -384,5 +333,6 @@ Item {
// so that the text aligned on all rows (the text might be different on each row) // so that the text aligned on all rows (the text might be different on each row)
property real pendingTextMaxWidth: 0 property real pendingTextMaxWidth: 0
} }
onPanelTypeChanged: { d.pendingTextMaxWidth = 0 } onPanelTypeChanged: { d.pendingTextMaxWidth = 0 }
} }

View File

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

View File

@ -25,46 +25,32 @@ StatusDialog {
Kick, Ban Kick, Ban
} }
width: 400 width: 480
title: root.mode === KickBanPopup.Mode.Kick title: root.mode === KickBanPopup.Mode.Kick
? qsTr("Kick %1").arg(root.username) ? qsTr("Kick %1").arg(root.username)
: qsTr("Ban %1").arg(root.username) : qsTr("Ban %1").arg(root.username)
contentItem: ColumnLayout { contentItem: ColumnLayout {
anchors.centerIn: parent
StatusBaseText { StatusBaseText {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
font.pixelSize: Theme.primaryTextFontSize
wrapMode: Text.Wrap wrapMode: Text.Wrap
text: root.mode === KickBanPopup.Mode.Kick text: root.mode === KickBanPopup.Mode.Kick
? qsTr("Are you sure you want to kick <b>%1</b> from %2?") ? qsTr("Are you sure you want to kick <b>%1</b> from %2?").arg(root.username).arg(root.communityName)
.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)
: 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 { StatusSwitch {
visible: root.mode === KickBanPopup.Mode.Ban Layout.fillWidth: true
id: deleteAllMessagesSwitch
StatusBaseText { visible: root.mode === KickBanPopup.Mode.Ban
Layout.fillWidth: true leftSide: false
text: qsTr("Delete all messages posted by the user")
text: qsTr("Delete all messages posted by the user")
font.pixelSize: Theme.primaryTextFontSize
}
StatusSwitch {
id: deleteAllMessagesSwitch
checked: false
}
}
} }
}
footer: StatusDialogFooter { footer: StatusDialogFooter {
rightButtons: ObjectModel { rightButtons: ObjectModel {
@ -74,8 +60,6 @@ StatusDialog {
onClicked: root.close() onClicked: root.close()
} }
StatusButton { StatusButton {
id: banButton
objectName: root.mode === KickBanPopup.Mode.Kick objectName: root.mode === KickBanPopup.Mode.Kick
? "CommunityMembers_KickModal_KickButton" ? "CommunityMembers_KickModal_KickButton"
: "CommunityMembers_BanModal_BanButton" : "CommunityMembers_BanModal_BanButton"

View File

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

View File

@ -1,49 +1,26 @@
import QtQuick 2.15 import QtQuick 2.15
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 utils 1.0 import shared.controls.delegates 1.0
StatusListItem { ContactListItemDelegate {
id: root 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 showSendMessageButton: false
property bool showRejectContactRequestButton: false property bool showRejectContactRequestButton: false
property bool showAcceptContactRequestButton: false property bool showAcceptContactRequestButton: false
property bool showRemoveRejectionButton: false
property string contactText: "" property string contactText: ""
signal contextMenuRequested signal contextMenuRequested
signal sendMessageRequested signal sendMessageRequested
signal showVerificationRequestRequested
signal acceptContactRequested signal acceptContactRequested
signal rejectRequestRequested signal rejectRequestRequested
signal removeRejectionRequested
asset.width: 40 icon.width: 40
asset.height: 40 icon.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)
}
components: [ components: [
StatusFlatRoundButton { StatusFlatRoundButton {
@ -53,6 +30,7 @@ StatusListItem {
height: visible ? 32 : 0 height: visible ? 32 : 0
icon.name: "chat" icon.name: "chat"
icon.color: Theme.palette.directColor1 icon.color: Theme.palette.directColor1
tooltip.text: qsTr("Send message")
onClicked: root.sendMessageRequested() onClicked: root.sendMessageRequested()
}, },
StatusFlatRoundButton { StatusFlatRoundButton {
@ -62,6 +40,7 @@ StatusListItem {
height: visible ? 32 : 0 height: visible ? 32 : 0
icon.name: "close-circle" icon.name: "close-circle"
icon.color: Theme.palette.dangerColor1 icon.color: Theme.palette.dangerColor1
tooltip.text: qsTr("Reject")
onClicked: root.rejectRequestRequested() onClicked: root.rejectRequestRequested()
}, },
StatusFlatRoundButton { StatusFlatRoundButton {
@ -71,26 +50,16 @@ StatusListItem {
height: visible ? 32 : 0 height: visible ? 32 : 0
icon.name: "checkmark-circle" icon.name: "checkmark-circle"
icon.color: Theme.palette.successColor1 icon.color: Theme.palette.successColor1
tooltip.text: qsTr("Accept")
onClicked: root.acceptContactRequested() 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 { StatusBaseText {
text: root.contactText text: root.contactText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
}, },
StatusFlatRoundButton { StatusFlatRoundButton {
objectName: "moreBtn" objectName: "moreBtn"
id: menuButton
width: 32 width: 32
height: 32 height: 32
icon.name: "more" icon.name: "more"

View File

@ -1,152 +1,71 @@
import QtQuick 2.15 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 0.1
import StatusQ.Core.Theme 0.1
import shared 1.0 import shared 1.0
import shared.panels 1.0
import shared.popups 1.0
import utils 1.0 import utils 1.0
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
Item { StatusListView {
id: root id: root
implicitHeight: (title.height + contactsList.height)
property var contactsModel
required property var contactsModel
property int panelUsage: Constants.contactsPanelUsage.unknownPosition property int panelUsage: Constants.contactsPanelUsage.unknownPosition
property string title: ""
property string searchString: "" property string searchString: ""
readonly property int count: contactsList.count
signal openContactContextMenu(string publicKey) signal openContactContextMenu(string publicKey)
signal sendMessageActionTriggered(string publicKey) signal sendMessageActionTriggered(string publicKey)
signal showVerificationRequest(string publicKey)
signal contactRequestAccepted(string publicKey) signal contactRequestAccepted(string publicKey)
signal contactRequestRejected(string publicKey) signal contactRequestRejected(string publicKey)
signal rejectionRemoved(string publicKey)
StyledText { objectName: "ContactListPanel_ListView"
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 { model: SortFilterProxyModel {
id: contactsList id: filteredModel
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 sourceModel: root.contactsModel
function panelUsagePredicate(isVerified) { filters: [
if (panelUsage === Constants.contactsPanelUsage.verifiedMutualContacts) UserSearchFilterContainer {
return isVerified searchString: root.searchString
if (panelUsage === Constants.contactsPanelUsage.mutualContacts)
return !isVerified
return true
} }
]
function searchPredicate(name, pubkey, compressedPubKey) { sorters: [
const lowerCaseSearchString = root.searchString.toLowerCase() FilterSorter { // Trusted contacts first
enabled: root.panelUsage === Constants.contactsPanelUsage.mutualContacts
return name.toLowerCase().includes(lowerCaseSearchString) || ValueFilter { roleName: "isVerified"; value: true }
pubkey.toLowerCase().includes(lowerCaseSearchString) || },
compressedPubKey.toLowerCase().includes(lowerCaseSearchString) FilterSorter { // Received CRs first
} id: pendingFilter
readonly property int received: Constants.ContactRequestState.Received
filters: [ enabled: root.panelUsage === Constants.contactsPanelUsage.pendingContacts
FastExpressionFilter { ValueFilter { roleName: "contactRequest"; value: pendingFilter.received }
expression: filteredModel.panelUsagePredicate(model.isVerified) },
expectedRoles: ["isVerified"] StringSorter {
},
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"]
}
]
sorters: StringSorter {
roleName: "preferredDisplayName" roleName: "preferredDisplayName"
caseSensitivity: Qt.CaseInsensitive caseSensitivity: Qt.CaseInsensitive
} }
} ]
}
delegate: ContactPanel { delegate: ContactPanel {
id: panelDelegate width: ListView.view.width
width: ListView.view.width showSendMessageButton: model.isContact && !model.isBlocked
name: model.preferredDisplayName showRejectContactRequestButton: root.panelUsage === Constants.contactsPanelUsage.pendingContacts &&
iconSource: model.thumbnailImage model.contactRequest === Constants.ContactRequestState.Received
showAcceptContactRequestButton: showRejectContactRequestButton
subTitle: model.ensVerified ? "" : Utils.getElidedCompressedPk(model.pubKey) contactText: root.panelUsage === Constants.contactsPanelUsage.pendingContacts &&
pubKeyColor: Utils.colorForPubkey(model.pubKey) model.contactRequest === Constants.ContactRequestState.Sent ? qsTr("Contact Request Sent")
colorHash: Utils.getColorHashAsJson(model.pubKey, model.ensVerified) : ""
showSendMessageButton: model.isContact && !model.isBlocked onClicked: Global.openProfilePopup(model.pubKey)
showRejectContactRequestButton: { onContextMenuRequested: root.openContactContextMenu(model.pubKey)
if (root.panelUsage === Constants.contactsPanelUsage.receivedContactRequest onSendMessageRequested: root.sendMessageActionTriggered(model.pubKey)
&& !model.verificationRequestStatus) onAcceptContactRequested: root.contactRequestAccepted(model.pubKey)
return true onRejectRequestRequested: root.contactRequestRejected(model.pubKey)
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 ""
}
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 ContactPanel 1.0 ContactPanel.qml
ContactsListPanel 1.0 ContactsListPanel.qml
ProfileDescriptionPanel 1.0 ProfileDescriptionPanel.qml ProfileDescriptionPanel 1.0 ProfileDescriptionPanel.qml
ProfileShowcaseAccountsPanel 1.0 ProfileShowcaseAccountsPanel.qml ProfileShowcaseAccountsPanel 1.0 ProfileShowcaseAccountsPanel.qml
ProfileShowcaseAssetsPanel 1.0 ProfileShowcaseAssetsPanel.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.Core.Theme 0.1
import StatusQ.Popups 0.1 import StatusQ.Popups 0.1
import "../stores" import AppLayouts.Profile.stores 1.0
StatusModal { StatusModal {
id: root id: root

View File

@ -10,3 +10,4 @@ TokenListPopup 1.0 TokenListPopup.qml
WalletKeypairAccountMenu 1.0 WalletKeypairAccountMenu.qml WalletKeypairAccountMenu 1.0 WalletKeypairAccountMenu.qml
WalletAddressMenu 1.0 WalletAddressMenu.qml WalletAddressMenu 1.0 WalletAddressMenu.qml
ConfirmChangePasswordModal 1.0 ConfirmChangePasswordModal.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 1.0
import shared.views.chat 1.0 import shared.views.chat 1.0
import "../stores" import AppLayouts.Profile.stores 1.0
import "../panels" import AppLayouts.Profile.panels 1.0
import "../popups" import AppLayouts.Profile.popups 1.0
SettingsContentBase { SettingsContentBase {
id: root id: root
@ -28,20 +28,17 @@ SettingsContentBase {
property ContactsStore contactsStore property ContactsStore contactsStore
property SharedStores.UtilsStore utilsStore property SharedStores.UtilsStore utilsStore
property var mutualContactsModel required property var mutualContactsModel
property var blockedContactsModel required property var blockedContactsModel
property var pendingReceivedRequestContactsModel required property var pendingContactsModel
property var pendingSentRequestContactsModel required property int pendingReceivedContactsCount
property alias searchStr: searchBox.text property alias searchStr: searchBox.text
property bool isPending: false
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: { onClicked: sendContactRequestComponent.createObject(root).open()
Global.openPopup(sendContactRequest);
}
} }
function openContextMenu(model, pubKey) { function openContextMenu(model, pubKey) {
@ -67,11 +64,108 @@ SettingsContentBase {
Global.openMenu(contactContextMenuComponent, this, params) Global.openMenu(contactContextMenuComponent, this, params)
} }
Item { headerComponents: ColumnLayout {
id: contentItem
width: root.contentWidth width: root.contentWidth
height: (searchBox.height + contactsTabBar.height spacing: Theme.padding
+ stackLayout.height + (2 * Theme.bigPadding))
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 { Component {
id: contactContextMenuComponent id: contactContextMenuComponent
@ -98,191 +192,27 @@ SettingsContentBase {
onClosed: destroy() onClosed: destroy()
} }
} }
SearchBox { }
id: searchBox
anchors.left: parent.left
anchors.right: parent.right
placeholderText: qsTr("Search by a display name or chat key")
}
StatusTabBar { component SectionComponent: Rectangle {
id: contactsTabBar required property string section
anchors.left: parent.left property alias text: sectionText.text
anchors.right: parent.right
anchors.top: searchBox.bottom
anchors.topMargin: Theme.padding
StatusTabButton { width: ListView.view.width
id: contactsBtn height: sectionText.implicitHeight
leftPadding: Theme.padding color: Theme.palette.statusListItem.backgroundColor
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")
}
}
StackLayout { StatusBaseText {
id: stackLayout id: sectionText
anchors.left: parent.left width: parent.width
anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter
anchors.top: contactsTabBar.bottom topPadding: Theme.halfPadding
currentIndex: contactsTabBar.currentIndex bottomPadding: Theme.halfPadding
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
Layout.fillWidth: true color: Theme.palette.baseColor1
title: qsTr("Trusted Contacts") font.pixelSize: Theme.additionalTextSize
visible: !noFriendsItem.visible && count > 0 font.weight: Font.Medium
contactsModel: root.mutualContactsModel elide: Text.ElideRight
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()
}
} }
} }
} }

View File

@ -2,8 +2,10 @@ AboutView 1.0 AboutView.qml
AppearanceView 1.0 AppearanceView.qml AppearanceView 1.0 AppearanceView.qml
ChangePasswordView 1.0 ChangePasswordView.qml ChangePasswordView 1.0 ChangePasswordView.qml
CommunitiesView 1.0 CommunitiesView.qml CommunitiesView 1.0 CommunitiesView.qml
ContactsView 1.0 ContactsView.qml
CurrenciesModel 1.0 CurrenciesModel.qml CurrenciesModel 1.0 CurrenciesModel.qml
LanguageView 1.0 LanguageView.qml LanguageView 1.0 LanguageView.qml
NotificationsView 1.0 NotificationsView.qml NotificationsView 1.0 NotificationsView.qml
PrivacyAndSecurityView 1.0 PrivacyAndSecurityView.qml PrivacyAndSecurityView 1.0 PrivacyAndSecurityView.qml
SyncingView 1.0 SyncingView.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) 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 = "" var text = ""
switch (state) { switch (state) {
case Constants.CommunityMembershipRequestState.Banned: case Constants.CommunityMembershipRequestState.Banned:
@ -1746,8 +1746,8 @@ Item {
mutualContactsModel: contactsModelAdaptor.mutualContacts mutualContactsModel: contactsModelAdaptor.mutualContacts
blockedContactsModel: contactsModelAdaptor.blockedContacts blockedContactsModel: contactsModelAdaptor.blockedContacts
pendingReceivedRequestContactsModel: contactsModelAdaptor.pendingReceivedRequestContacts pendingContactsModel: contactsModelAdaptor.pendingContacts
pendingSentRequestContactsModel: contactsModelAdaptor.pendingSentRequestContacts pendingReceivedContactsCount: contactsModelAdaptor.pendingReceivedRequestContacts.count
Binding on settingsSubsection { Binding on settingsSubsection {
value: profileLoader.settingsSubsection value: profileLoader.settingsSubsection

View File

@ -19,7 +19,7 @@ QObject {
localNickname [string] - local nickname set by the current user localNickname [string] - local nickname set by the current user
alias [string] - generated 3 word name alias [string] - generated 3 word name
icon [string] - thumbnail image of the user 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 colorHash [string] - generated color hash for the user's profile
onlineStatus [int] - the online status of the member onlineStatus [int] - the online status of the member
isContact [bool] - whether the user is a mutual contact or not isContact [bool] - whether the user is a mutual contact or not
@ -75,4 +75,21 @@ QObject {
value: Constants.ContactRequestState.Sent 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 pubKey: model.isEnsVerified ? "" : model.compressedPubKey
nickName: model.localNickname nickName: model.localNickname
userName: ProfileUtils.displayName("", model.ensName, model.displayName, model.alias) userName: ProfileUtils.displayName("", model.ensName, model.displayName, model.alias)
isVerified: model.isVerified isBlocked: model.isBlocked
isUntrustworthy: model.isUntrustworthy isVerified: model.isVerified || model.trustStatus === Constants.trustStatus.trusted
isUntrustworthy: model.isUntrustworthy || model.trustStatus === Constants.trustStatus.untrustworthy
isContact: model.isContact isContact: model.isContact
icon.name: model.icon icon.name: model.thumbnailImage || model.icon
icon.color: Utils.colorForColorId(model.colorId) icon.color: Utils.colorForColorId(model.colorId)
status: model.onlineStatus status: model.onlineStatus
ringSettings.ringSpecModel: model.colorHash ringSettings.ringSpecModel: model.colorHash

View File

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

View File

@ -1,31 +1,31 @@
import QtQuick 2.15 import QtQuick 2.15
import utils 1.0
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import "../popups" import utils 1.0
import shared.popups 1.0
Item { Item {
id: noContactsRect id: root
implicitWidth: 260 implicitWidth: 260
implicitHeight: visible ? 120 : 0 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 alias textColor: noContacts.color
property bool inviteButtonVisible: true
StatusBaseText { StatusBaseText {
id: noContacts id: noContacts
text: noContactsRect.text text: root.text
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: Theme.padding anchors.topMargin: Theme.padding
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
font.pixelSize: 15
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
StatusButton { StatusButton {
@ -33,7 +33,8 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: noContacts.bottom anchors.top: noContacts.bottom
anchors.topMargin: Theme.padding anchors.topMargin: Theme.padding
onClicked: Global.openPopup(inviteFriendsPopup); visible: root.inviteButtonVisible
onClicked: inviteFriendsPopup.createObject(root).open()
} }
Component { Component {

View File

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