diff --git a/storybook/pages/ContactsViewPage.qml b/storybook/pages/ContactsViewPage.qml
new file mode 100644
index 00000000000..61fc2da8256
--- /dev/null
+++ b/storybook/pages/ContactsViewPage.qml
@@ -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
diff --git a/storybook/pages/MembersTabPanelPage.qml b/storybook/pages/MembersTabPanelPage.qml
index 0a88079f920..03fa8e2833d 100644
--- a/storybook/pages/MembersTabPanelPage.qml
+++ b/storybook/pages/MembersTabPanelPage.qml
@@ -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/KubaโDesktop?type=design&node-id=35909-605774&mode=design&t=KfrAekLfW5mTy68x-0
diff --git a/storybook/pages/StatusTabBarPage.qml b/storybook/pages/StatusTabBarPage.qml
index 33b5692eae7..44e4ecbc99c 100644
--- a/storybook/pages/StatusTabBarPage.qml
+++ b/storybook/pages/StatusTabBarPage.qml
@@ -29,7 +29,7 @@ Item {
StatusTabButton {
width: implicitWidth
enabled: false
- text: qsTr("Blocked & disabled")
+ text: "Blocked & disabled"
}
StatusTabButton {
width: implicitWidth
diff --git a/storybook/src/Models/UsersModel.qml b/storybook/src/Models/UsersModel.qml
index 6101baf4611..03c6e6fcf1f 100644
--- a/storybook/src/Models/UsersModel.qml
+++ b/storybook/src/Models/UsersModel.qml
@@ -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: "",
diff --git a/storybook/src/Storybook/CheckBoxFlowSelector.qml b/storybook/src/Storybook/CheckBoxFlowSelector.qml
index fea8314d21a..d309d1a59ae 100644
--- a/storybook/src/Storybook/CheckBoxFlowSelector.qml
+++ b/storybook/src/Storybook/CheckBoxFlowSelector.qml
@@ -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
diff --git a/test/e2e/gui/objects_map/settings_names.py b/test/e2e/gui/objects_map/settings_names.py
index 063c0c43f1d..4b5ae1c94f7 100644
--- a/test/e2e/gui/objects_map/settings_names.py
+++ b/test/e2e/gui/objects_map/settings_names.py
@@ -52,7 +52,7 @@
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}
diff --git a/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml b/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml
index 64127d7a1d5..23ee4a731a7 100644
--- a/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml
+++ b/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml
@@ -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
}
diff --git a/ui/StatusQ/src/StatusQ/Components/StatusMemberListItem.qml b/ui/StatusQ/src/StatusQ/Components/StatusMemberListItem.qml
index ddc3e4acef2..d4c12077735 100644
--- a/ui/StatusQ/src/StatusQ/Components/StatusMemberListItem.qml
+++ b/ui/StatusQ/src/StatusQ/Components/StatusMemberListItem.qml
@@ -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
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml b/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml
index 8179c0b5f14..66e0ef155e0 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusClearButton.qml
@@ -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")
}
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusFlatRoundButton.qml b/ui/StatusQ/src/StatusQ/Controls/StatusFlatRoundButton.qml
index 360ce592f77..ef976e7e2ad 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusFlatRoundButton.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusFlatRoundButton.qml
@@ -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: {
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusIconSwitch.qml b/ui/StatusQ/src/StatusQ/Controls/StatusIconSwitch.qml
index 8c2daaeb2fd..b245236f345 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusIconSwitch.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusIconSwitch.qml
@@ -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
+ Layout.fillHeight: true
StatusBaseText {
+ Layout.fillWidth: true
text: root.title
+ visible: !!text
color: Theme.palette.directColor1
- font.pixelSize: 15
+ elide: Text.ElideRight
}
- Item { Layout.fillWidth: true }
-
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()
}
diff --git a/ui/app/AppLayouts/Communities/layouts/SettingsPage.qml b/ui/app/AppLayouts/Communities/layouts/SettingsPage.qml
index 24fc7be2940..c8fa231874d 100644
--- a/ui/app/AppLayouts/Communities/layouts/SettingsPage.qml
+++ b/ui/app/AppLayouts/Communities/layouts/SettingsPage.qml
@@ -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
}
diff --git a/ui/app/AppLayouts/Communities/panels/MembersSettingsPanel.qml b/ui/app/AppLayouts/Communities/panels/MembersSettingsPanel.qml
index 061d0612c3d..cd289a1ad83 100644
--- a/ui/app/AppLayouts/Communities/panels/MembersSettingsPanel.qml
+++ b/ui/app/AppLayouts/Communities/panels/MembersSettingsPanel.qml
@@ -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: Constants.CommunityMembershipSubSections.Members
+
id: allMembersBtn
objectName: "allMembersButton"
width: implicitWidth
@@ -87,6 +90,8 @@ SettingsPage {
}
StatusTabButton {
+ readonly property int subSection: Constants.CommunityMembershipSubSections.MembershipRequests
+
id: pendingRequestsBtn
objectName: "pendingRequestsButton"
width: implicitWidth
@@ -95,6 +100,8 @@ SettingsPage {
}
StatusTabButton {
+ readonly property int subSection: Constants.CommunityMembershipSubSections.RejectedMembers
+
id: declinedRequestsBtn
objectName: "declinedRequestsButton"
width: implicitWidth
@@ -103,6 +110,8 @@ SettingsPage {
}
StatusTabButton {
+ readonly property int subSection: Constants.CommunityMembershipSubSections.BannedMembers
+
id: bannedBtn
objectName: "bannedButton"
width: implicitWidth
@@ -111,79 +120,53 @@ SettingsPage {
}
}
- StackLayout {
- id: stackLayout
+ SearchBox {
+ id: memberSearch
+ Layout.preferredWidth: root.preferredContentWidth
+ placeholderText: qsTr("Search by name or chat key")
+ enabled: membersTabBar.currentItem.enabled
+ }
+
+ MembersTabPanel {
Layout.fillWidth: true
Layout.fillHeight: true
- currentIndex: membersTabBar.currentIndex
-
- MembersTabPanel {
- model: root.membersModel
- 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
- kickBanPopup.username = name
- kickBanPopup.userId = id
- kickBanPopup.open()
- }
- onBanUserClicked: {
- kickBanPopup.mode = KickBanPopup.Mode.Ban
- kickBanPopup.username = name
- kickBanPopup.userId = id
- kickBanPopup.open()
+ 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
}
-
- onViewMemberMessagesClicked: root.viewMemberMessagesClicked(pubKey, displayName)
}
- MembersTabPanel {
- model: root.pendingMembersModel
- rootStore: root.rootStore
- utilsStore: root.utilsStore
- memberRole: root.memberRole
- panelType: MembersTabPanel.TabType.PendingRequests
+ searchString: memberSearch.text
+ rootStore: root.rootStore
+ utilsStore: root.utilsStore
+ memberRole: root.memberRole
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- 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)
+ onKickUserClicked: {
+ kickBanPopup.mode = KickBanPopup.Mode.Kick
+ kickBanPopup.username = name
+ kickBanPopup.userId = id
+ kickBanPopup.open()
}
-
- 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)
+ onBanUserClicked: {
+ kickBanPopup.mode = KickBanPopup.Mode.Ban
+ kickBanPopup.username = name
+ kickBanPopup.userId = id
+ kickBanPopup.open()
}
+ onUnbanUserClicked: root.unbanUserClicked(id)
+ onAcceptRequestToJoin: root.acceptRequestToJoin(id)
+ onDeclineRequestToJoin: root.declineRequestToJoin(id)
+ onViewMemberMessagesClicked: root.viewMemberMessagesClicked(pubKey, displayName)
}
}
diff --git a/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml b/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml
index 1aa572c807a..435cab7e097 100644
--- a/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml
+++ b/ui/app/AppLayouts/Communities/panels/MembersTabPanel.qml
@@ -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,332 +51,279 @@ Item {
DeclinedRequests
}
- property int panelType: MembersTabPanel.TabType.AllMembers
-
- ColumnLayout {
+ StatusListView {
+ objectName: "CommunityMembersTabPanel_MembersListViews"
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"
+ model: SortFilterProxyModel {
+ sourceModel: root.model
- Layout.fillWidth: true
- Layout.fillHeight: true
-
- model: SortFilterProxyModel {
- id: filteredModel
- sourceModel: root.model
-
- function searchPredicate(ensName, displayName, aliasName) {
- const lowerCaseSearchString = memberSearch.text.toLowerCase()
- const secondaryName = ProfileUtils.displayName("", ensName, displayName, aliasName)
+ sorters: [
+ StringSorter {
+ roleName: "preferredDisplayName"
+ caseSensitivity: Qt.CaseInsensitive
+ }
+ ]
- return secondaryName.toLowerCase().includes(lowerCaseSearchString)
+ filters: [
+ UserFilterContainer {
+ searchString: root.searchString
}
+ ]
+ }
- 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
- }
+ spacing: 0
+
+ delegate: ContactListItemDelegate {
+ id: memberItem
+
+ // Buttons visibility conditions:
+ // 1. Tab based buttons - only visible when the tab is selected
+ // 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
+
+ /// Helpers ///
+
+ // 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)
+
+
+ // 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 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
-
- delegate: StatusMemberListItem {
- id: memberItem
-
- // Buttons visibility conditions:
- // 1. Tab based buttons - only visible when the tab is selected
- // 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
-
- /// Helpers ///
-
- // 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)
-
-
- // 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
+ readonly property bool showOnHover: hovered && ctaAllowed
+ readonly property bool canDeleteMessages: model.isCurrentUser || model.memberRole !== Constants.memberRole.owner
+
+ /// Button visibility ///
+ 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 ///
+ 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
+
+ components: [
+ StatusBaseText {
+ id: pendingText
+ width: Math.max(implicitWidth, d.pendingTextMaxWidth)
+ onImplicitWidthChanged: {
+ d.pendingTextMaxWidth = Math.max(implicitWidth, d.pendingTextMaxWidth)
}
- }
- readonly property bool showOnHover: isHovered && ctaAllowed
- readonly property bool canDeleteMessages: model.isCurrentUser || model.memberRole !== Constants.memberRole.owner
-
- /// Button visibility ///
- 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 ///
- 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
-
- rightPadding: 75
- leftPadding: 12
-
- components: [
- StatusBaseText {
- id: pendingText
- width: Math.max(implicitWidth, d.pendingTextMaxWidth)
- onImplicitWidthChanged: {
- d.pendingTextMaxWidth = Math.max(implicitWidth, d.pendingTextMaxWidth)
- }
- 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 {
- 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)
+ 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
}
- ]
-
- readonly property string title: model.preferredDisplayName
-
- width: membersList.width
- 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)
+ },
+
+ 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 string title: model.preferredDisplayName
+
+ width: ListView.view.width
+
+ icon.width: 40
+ icon.height: 40
+
+ 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
}
+
+ memberContextMenuComponent.createObject(root, params).popup(this)
+ } else if (mouse.button === Qt.LeftButton) {
+ Global.openProfilePopup(model.pubKey)
}
}
}
- }
- Component {
- id: memberContextMenuComponent
+ Component {
+ id: memberContextMenuComponent
- ProfileContextMenu {
- id: memberContextMenuView
+ ProfileContextMenu {
+ id: memberContextMenuView
- required property string pubKey
+ required property string pubKey
- onOpenProfileClicked: Global.openProfilePopup(pubKey, null)
- onCreateOneToOneChat: {
- Global.changeAppSectionBySectionType(Constants.appSection.chat)
- root.rootStore.chatCommunitySectionModule.createOneToOneChat("", pubKey, "")
+ onOpenProfileClicked: Global.openProfilePopup(pubKey, null)
+ onCreateOneToOneChat: {
+ Global.changeAppSectionBySectionType(Constants.appSection.chat)
+ 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)
property real pendingTextMaxWidth: 0
}
+
onPanelTypeChanged: { d.pendingTextMaxWidth = 0 }
}
diff --git a/ui/app/AppLayouts/Communities/popups/CommunityMemberMessagesPopup.qml b/ui/app/AppLayouts/Communities/popups/CommunityMemberMessagesPopup.qml
index 3411830debf..020a773322c 100644
--- a/ui/app/AppLayouts/Communities/popups/CommunityMemberMessagesPopup.qml
+++ b/ui/app/AppLayouts/Communities/popups/CommunityMemberMessagesPopup.qml
@@ -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
diff --git a/ui/app/AppLayouts/Communities/popups/KickBanPopup.qml b/ui/app/AppLayouts/Communities/popups/KickBanPopup.qml
index edc7e562e0c..ece3a5fc6c3 100644
--- a/ui/app/AppLayouts/Communities/popups/KickBanPopup.qml
+++ b/ui/app/AppLayouts/Communities/popups/KickBanPopup.qml
@@ -25,46 +25,32 @@ 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 %1 from %2?")
- .arg(root.username).arg(root.communityName)
- : qsTr("Are you sure you want to ban %1 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 kick %1 from %2?").arg(root.username).arg(root.communityName)
+ : qsTr("Are you sure you want to ban %1 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
- }
-
- StatusSwitch {
- id: deleteAllMessagesSwitch
-
- checked: false
- }
- }
+ StatusSwitch {
+ Layout.fillWidth: true
+ id: deleteAllMessagesSwitch
+ visible: root.mode === KickBanPopup.Mode.Ban
+ leftSide: false
+ text: qsTr("Delete all messages posted by the user")
}
+ }
footer: StatusDialogFooter {
rightButtons: ObjectModel {
@@ -74,8 +60,6 @@ StatusDialog {
onClicked: root.close()
}
StatusButton {
- id: banButton
-
objectName: root.mode === KickBanPopup.Mode.Kick
? "CommunityMembers_KickModal_KickButton"
: "CommunityMembers_BanModal_BanButton"
diff --git a/ui/app/AppLayouts/Profile/ProfileLayout.qml b/ui/app/AppLayouts/Profile/ProfileLayout.qml
index e75ca513bee..990184282a8 100644
--- a/ui/app/AppLayouts/Profile/ProfileLayout.qml
+++ b/ui/app/AppLayouts/Profile/ProfileLayout.qml
@@ -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
}
}
diff --git a/ui/app/AppLayouts/Profile/panels/ContactPanel.qml b/ui/app/AppLayouts/Profile/panels/ContactPanel.qml
index 428f0d07d94..e77553f71c8 100644
--- a/ui/app/AppLayouts/Profile/panels/ContactPanel.qml
+++ b/ui/app/AppLayouts/Profile/panels/ContactPanel.qml
@@ -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"
diff --git a/ui/app/AppLayouts/Profile/panels/ContactsListPanel.qml b/ui/app/AppLayouts/Profile/panels/ContactsListPanel.qml
index 09729068553..d8b0676016f 100644
--- a/ui/app/AppLayouts/Profile/panels/ContactsListPanel.qml
+++ b/ui/app/AppLayouts/Profile/panels/ContactsListPanel.qml
@@ -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
+ objectName: "ContactListPanel_ListView"
- sourceModel: root.contactsModel
+ model: SortFilterProxyModel {
+ id: filteredModel
- function panelUsagePredicate(isVerified) {
- if (panelUsage === Constants.contactsPanelUsage.verifiedMutualContacts)
- return isVerified
- if (panelUsage === Constants.contactsPanelUsage.mutualContacts)
- return !isVerified
+ sourceModel: root.contactsModel
- return true
+ filters: [
+ UserFilterContainer {
+ searchString: root.searchString
}
-
- 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"]
- }
- ]
-
- 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")
+ delegate: ContactPanel {
+ width: ListView.view.width
- return ""
- }
+ showSendMessageButton: model.isContact && !model.isBlocked
+ 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")
+ : ""
- 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)
- }
+ 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)
}
}
diff --git a/ui/app/AppLayouts/Profile/panels/qmldir b/ui/app/AppLayouts/Profile/panels/qmldir
index 57d88abc5f1..2e55cb8d022 100644
--- a/ui/app/AppLayouts/Profile/panels/qmldir
+++ b/ui/app/AppLayouts/Profile/panels/qmldir
@@ -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
diff --git a/ui/app/AppLayouts/Profile/popups/SendContactRequestModal.qml b/ui/app/AppLayouts/Profile/popups/SendContactRequestModal.qml
index b9c5666b626..e3893079546 100644
--- a/ui/app/AppLayouts/Profile/popups/SendContactRequestModal.qml
+++ b/ui/app/AppLayouts/Profile/popups/SendContactRequestModal.qml
@@ -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
diff --git a/ui/app/AppLayouts/Profile/popups/qmldir b/ui/app/AppLayouts/Profile/popups/qmldir
index 98340156cb6..4847913aa9a 100644
--- a/ui/app/AppLayouts/Profile/popups/qmldir
+++ b/ui/app/AppLayouts/Profile/popups/qmldir
@@ -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
diff --git a/ui/app/AppLayouts/Profile/views/ContactsView.qml b/ui/app/AppLayouts/Profile/views/ContactsView.qml
index 38e3641aead..d1741a519ab 100644
--- a/ui/app/AppLayouts/Profile/views/ContactsView.qml
+++ b/ui/app/AppLayouts/Profile/views/ContactsView.qml
@@ -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,222 +64,156 @@ SettingsContentBase {
Global.openMenu(contactContextMenuComponent, this, params)
}
- Item {
- id: contentItem
+ headerComponents: ColumnLayout {
width: root.contentWidth
- height: (searchBox.height + contactsTabBar.height
- + stackLayout.height + (2 * Theme.bigPadding))
-
- Component {
- id: contactContextMenuComponent
- ProfileContextMenu {
- id: contactContextMenu
-
- property string pubKey
-
- onOpenProfileClicked: Global.openProfilePopup(contactContextMenu.pubKey, null, null)
- onReviewContactRequest: Global.openReviewContactRequestPopup(contactContextMenu.pubKey, null)
- onSendContactRequest: Global.openContactRequestPopup(contactContextMenu.pubKey, null)
- onEditNickname: Global.openNicknamePopupRequested(contactContextMenu.pubKey, null)
- onUnblockContact: Global.unblockContactRequested(contactContextMenu.pubKey)
- onMarkAsUntrusted: Global.markAsUntrustedRequested(contactContextMenu.pubKey)
- onRemoveContact: Global.removeContactRequested(contactContextMenu.pubKey)
- onBlockContact: Global.blockContactRequested(contactContextMenu.pubKey)
-
- onCreateOneToOneChat: root.contactsStore.joinPrivateChat(contactContextMenu.pubKey)
- onRemoveTrustStatus: root.contactsStore.removeTrustStatus(contactContextMenu.pubKey)
- onRemoveNickname: root.contactsStore.changeContactNickname(contactContextMenu.pubKey, "",
- contactContextMenu.displayName, true)
- onMarkAsTrusted: Global.openMarkAsIDVerifiedPopup(contactContextMenu.pubKey, null)
- onRemoveTrustedMark: Global.openRemoveIDVerificationDialog(contactContextMenu.pubKey, null)
- onClosed: destroy()
- }
- }
- SearchBox {
- id: searchBox
- anchors.left: parent.left
- anchors.right: parent.right
- placeholderText: qsTr("Search by a display name or chat key")
- }
+ spacing: Theme.padding
StatusTabBar {
id: contactsTabBar
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.top: searchBox.bottom
- anchors.topMargin: Theme.padding
+ Layout.fillWidth: true
StatusTabButton {
- id: contactsBtn
- leftPadding: Theme.padding
+ readonly property int panelUsage: Constants.contactsPanelUsage.mutualContacts
+
width: implicitWidth
text: qsTr("Contacts")
}
StatusTabButton {
- id: pendingRequestsBtn
+ readonly property int panelUsage: Constants.contactsPanelUsage.pendingContacts
+
objectName: "ContactsView_PendingRequest_Button"
width: implicitWidth
- enabled: !root.pendingReceivedRequestContactsModel.ModelCount.empty ||
- !root.pendingSentRequestContactsModel.ModelCount.empty
+ enabled: !!root.pendingContactsModel && !root.pendingContactsModel.ModelCount.empty
text: qsTr("Pending Requests")
- badge.value: root.pendingReceivedRequestContactsModel.ModelCount.count
+ badge.value: root.pendingReceivedContactsCount
}
StatusTabButton {
- id: blockedBtn
+ readonly property int panelUsage: Constants.contactsPanelUsage.blockedContacts
+
objectName: "ContactsView_Blocked_Button"
width: implicitWidth
- enabled: !root.blockedContactsModel.ModelCount.empty
+ enabled: !!root.blockedContactsModel && !root.blockedContactsModel.ModelCount.empty
text: qsTr("Blocked")
}
}
- 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
-
- 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
+ SearchBox {
+ id: searchBox
+ Layout.fillWidth: true
+ placeholderText: qsTr("Search by name or chat key")
+ }
+ }
- NoFriendsRectangle {
- anchors.centerIn: parent
- text: qsTr("You don't have any contacts yet")
- }
- }
+ ContactsListPanel {
+ id: contactsListPanel
+ width: root.contentWidth
+ height: root.availableHeight
+ objectName: "ContactsListPanel"
+
+ 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
}
-
- // 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
- }
+ }
+ section.property: {
+ switch (contactsListPanel.panelUsage) {
+ case Constants.contactsPanelUsage.pendingContacts:
+ return "contactRequest"
+ case Constants.contactsPanelUsage.blockedContacts:
+ return ""
+ case Constants.contactsPanelUsage.mutualContacts:
+ default:
+ return "isVerified"
}
-
- // 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;
- }
+ }
+ section.delegate: SectionComponent {
+ text: {
+ switch (contactsListPanel.panelUsage) {
+ case Constants.contactsPanelUsage.pendingContacts:
+ return section === `${Constants.ContactRequestState.Received}` ? qsTr("Received") : qsTr("Sent")
+ case Constants.contactsPanelUsage.blockedContacts:
+ return ""
+ case Constants.contactsPanelUsage.mutualContacts:
+ default:
+ return section === "true" ? qsTr("Trusted Contacts") : qsTr("Contacts")
}
}
}
+ section.labelPositioning: ViewSection.InlineLabels | ViewSection.CurrentLabelAtStart
- Component {
- id: loadingIndicator
- StatusLoadingIndicator {
- width: 12
- height: 12
- }
+ 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: sendContactRequest
+ id: sendContactRequestComponent
SendContactRequestModal {
contactsStore: root.contactsStore
onClosed: destroy()
}
}
+
+ Component {
+ id: contactContextMenuComponent
+ ProfileContextMenu {
+ id: contactContextMenu
+
+ property string pubKey
+
+ onOpenProfileClicked: Global.openProfilePopup(contactContextMenu.pubKey, null, null)
+ onReviewContactRequest: Global.openReviewContactRequestPopup(contactContextMenu.pubKey, null)
+ onSendContactRequest: Global.openContactRequestPopup(contactContextMenu.pubKey, null)
+ onEditNickname: Global.openNicknamePopupRequested(contactContextMenu.pubKey, null)
+ onUnblockContact: Global.unblockContactRequested(contactContextMenu.pubKey)
+ onMarkAsUntrusted: Global.markAsUntrustedRequested(contactContextMenu.pubKey)
+ onRemoveContact: Global.removeContactRequested(contactContextMenu.pubKey)
+ onBlockContact: Global.blockContactRequested(contactContextMenu.pubKey)
+
+ onCreateOneToOneChat: root.contactsStore.joinPrivateChat(contactContextMenu.pubKey)
+ onRemoveTrustStatus: root.contactsStore.removeTrustStatus(contactContextMenu.pubKey)
+ onRemoveNickname: root.contactsStore.changeContactNickname(contactContextMenu.pubKey, "",
+ contactContextMenu.displayName, true)
+ onMarkAsTrusted: Global.openMarkAsIDVerifiedPopup(contactContextMenu.pubKey, null)
+ onRemoveTrustedMark: Global.openRemoveIDVerificationDialog(contactContextMenu.pubKey, null)
+ onClosed: destroy()
+ }
+ }
+ }
+
+ component SectionComponent: Rectangle {
+ required property string section
+ property alias text: sectionText.text
+
+ width: ListView.view.width
+ height: sectionText.implicitHeight
+ color: Theme.palette.statusListItem.backgroundColor
+
+ StatusBaseText {
+ id: sectionText
+ width: parent.width
+ anchors.verticalCenter: parent.verticalCenter
+ topPadding: Theme.halfPadding
+ bottomPadding: Theme.halfPadding
+
+ color: Theme.palette.baseColor1
+ font.pixelSize: Theme.additionalTextSize
+ font.weight: Font.Medium
+ elide: Text.ElideRight
+ }
}
}
diff --git a/ui/app/AppLayouts/Profile/views/qmldir b/ui/app/AppLayouts/Profile/views/qmldir
index ae9757614bc..fcb0bd7d3f9 100644
--- a/ui/app/AppLayouts/Profile/views/qmldir
+++ b/ui/app/AppLayouts/Profile/views/qmldir
@@ -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
diff --git a/ui/app/mainui/AppMain.qml b/ui/app/mainui/AppMain.qml
index b09f5252027..9821e720d98 100644
--- a/ui/app/mainui/AppMain.qml
+++ b/ui/app/mainui/AppMain.qml
@@ -538,7 +538,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:
@@ -1724,8 +1724,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
diff --git a/ui/app/mainui/adaptors/ContactsModelAdaptor.qml b/ui/app/mainui/adaptors/ContactsModelAdaptor.qml
index 85a43ab0408..7e433d6d054 100644
--- a/ui/app/mainui/adaptors/ContactsModelAdaptor.qml
+++ b/ui/app/mainui/adaptors/ContactsModelAdaptor.qml
@@ -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
+ }
+ }
+ ]
+ }
}
diff --git a/ui/imports/shared/UserFilterContainer.qml b/ui/imports/shared/UserFilterContainer.qml
new file mode 100644
index 00000000000..16e423f9e85
--- /dev/null
+++ b/ui/imports/shared/UserFilterContainer.qml
@@ -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
+ }
+}
diff --git a/ui/imports/shared/controls/delegates/ContactListItemDelegate.qml b/ui/imports/shared/controls/delegates/ContactListItemDelegate.qml
index 9a34140d8f2..d1ac919d95f 100644
--- a/ui/imports/shared/controls/delegates/ContactListItemDelegate.qml
+++ b/ui/imports/shared/controls/delegates/ContactListItemDelegate.qml
@@ -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
diff --git a/ui/imports/shared/qmldir b/ui/imports/shared/qmldir
index 3f7a69d726b..1e2cf900774 100644
--- a/ui/imports/shared/qmldir
+++ b/ui/imports/shared/qmldir
@@ -3,3 +3,4 @@ module shared
DelegateModelGeneralized 1.0 DelegateModelGeneralized.qml
LoadingAnimation 1.0 LoadingAnimation.qml
MacTrafficLights 1.0 MacTrafficLights.qml
+UserFilterContainer 1.0 UserFilterContainer.qml
diff --git a/ui/imports/shared/views/NoFriendsRectangle.qml b/ui/imports/shared/views/NoFriendsRectangle.qml
index 3e56315405d..b1ef3b17e69 100644
--- a/ui/imports/shared/views/NoFriendsRectangle.qml
+++ b/ui/imports/shared/views/NoFriendsRectangle.qml
@@ -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 donโt have any contacts yet. Invite your friends to start chatting.")
+ property string text: inviteButtonVisible ? qsTr("You donโt 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 {
diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml
index f44a611dbc3..2694af97c9b 100644
--- a/ui/imports/utils/Constants.qml
+++ b/ui/imports/utils/Constants.qml
@@ -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 {