diff --git a/storybook/pages/ContactsViewPage.qml b/storybook/pages/ContactsViewPage.qml new file mode 100644 index 0000000000..61fc2da825 --- /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 0a88079f92..03fa8e2833 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 33b5692eae..44e4ecbc99 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 6101baf461..03c6e6fcf1 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 fea8314d21..d309d1a59a 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 063c0c43f1..4b5ae1c94f 100644 --- a/test/e2e/gui/objects_map/settings_names.py +++ b/test/e2e/gui/objects_map/settings_names.py @@ -52,7 +52,7 @@ settingsContentBaseScrollView_ContactListPanel = {"container": mainWindow_Contac settingsContentBaseScrollView_Item = {"container": mainWindow_ContactsView, "type": "Item", "unnamed": 1, "visible": True} settingsContentBaseScrollView_sentRequests_ContactsListPanel = {"container": mainWindow_ContactsView, "objectName": "sentRequests_ContactsListPanel", "type": "ContactsListPanel", "visible": True} contactsTabBar_Contacts_StatusTabButton = {"container": mainWindow_ContactsView, "id": "contactsBtn", "type": "StatusTabButton", "unnamed": 1, "visible": True} -settingsContentBaseScrollView_receivedRequests_ContactsListPanel = {"container": mainWindow_ContactsView, "objectName": "receivedRequests_ContactsListPanel", "type": "ContactsListPanel", "visible": True} +settingsContentBaseScrollView_receivedRequests_ContactsListPanel = {"container": mainWindow_ContactsView, "objectName": "ContactsListPanel", "type": "ContactsListPanel", "visible": True} settingsContentBaseScrollView_mutualContacts_ContactsListPanel = {"container": mainWindow_ContactsView, "id": "mutualContacts", "type": "ContactsListPanel", "unnamed": 1, "visible": True} settingsContentBaseScrollView_Invite_friends_StatusButton = {"container": mainWindow_ContactsView, "type": "StatusButton", "unnamed": 1, "visible": True} settingsContentBaseScrollView_NoFriendsRectangle = {"container": mainWindow_ContactsView, "type": "NoFriendsRectangle", "unnamed": 1, "visible": True} diff --git a/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml b/ui/StatusQ/src/StatusQ/Components/StatusContactVerificationIcons.qml index 64127d7a1d..23ee4a731a 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 ddc3e4acef..d4c1207773 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 8179c0b5f1..66e0ef155e 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 360ce592f7..ef976e7e2a 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 8c2daaeb2f..b245236f34 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 - - StatusBaseText { - text: root.title - color: Theme.palette.directColor1 - font.pixelSize: 15 - } - - Item { Layout.fillWidth: true } + Layout.fillHeight: true + + StatusBaseText { + Layout.fillWidth: true + text: root.title + visible: !!text + color: Theme.palette.directColor1 + elide: Text.ElideRight + } StatusBaseText { Layout.fillWidth: true - Layout.fillHeight: true text: root.subTitle visible: !!text color: Theme.palette.baseColor1 - font.pixelSize: 15 lineHeight: 1.2 wrapMode: Text.WordWrap elide: Text.ElideRight @@ -51,6 +52,7 @@ Control { StatusSwitch { id: switchItem objectName: "switchItem" + padding: 0 onToggled: root.toggled() } diff --git a/ui/app/AppLayouts/Communities/layouts/SettingsPage.qml b/ui/app/AppLayouts/Communities/layouts/SettingsPage.qml index 24fc7be294..c8fa231874 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 061d0612c3..dd73ff7e15 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: MembersTabPanel.TabType.AllMembers + id: allMembersBtn objectName: "allMembersButton" width: implicitWidth @@ -87,6 +90,8 @@ SettingsPage { } StatusTabButton { + readonly property int subSection: MembersTabPanel.TabType.PendingRequests + id: pendingRequestsBtn objectName: "pendingRequestsButton" width: implicitWidth @@ -95,6 +100,8 @@ SettingsPage { } StatusTabButton { + readonly property int subSection: MembersTabPanel.TabType.DeclinedRequests + id: declinedRequestsBtn objectName: "declinedRequestsButton" width: implicitWidth @@ -103,6 +110,8 @@ SettingsPage { } StatusTabButton { + readonly property int subSection: MembersTabPanel.TabType.BannedMembers + id: bannedBtn objectName: "bannedButton" width: implicitWidth @@ -111,79 +120,53 @@ SettingsPage { } } - StackLayout { - id: stackLayout - Layout.fillWidth: true + SearchBox { + id: memberSearch + Layout.preferredWidth: root.preferredContentWidth + placeholderText: qsTr("Search by name or chat key") + enabled: membersTabBar.currentItem.enabled + } + + MembersTabPanel { + Layout.preferredWidth: root.preferredContentWidth 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() + 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 } - - onBanUserClicked: { - kickBanPopup.mode = KickBanPopup.Mode.Ban - kickBanPopup.username = name - kickBanPopup.userId = id - kickBanPopup.open() - } - - onViewMemberMessagesClicked: root.viewMemberMessagesClicked(pubKey, displayName) } - MembersTabPanel { - model: root.pendingMembersModel - rootStore: root.rootStore - utilsStore: root.utilsStore - memberRole: root.memberRole - panelType: MembersTabPanel.TabType.PendingRequests + 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) + onKickUserClicked: { + kickBanPopup.mode = KickBanPopup.Mode.Kick + kickBanPopup.username = name + kickBanPopup.userId = id + kickBanPopup.open() } - - MembersTabPanel { - model: root.declinedMembersModel - rootStore: root.rootStore - utilsStore: root.utilsStore - memberRole: root.memberRole - panelType: MembersTabPanel.TabType.DeclinedRequests - - Layout.fillWidth: true - Layout.fillHeight: true - - onAcceptRequestToJoin: root.acceptRequestToJoin(id) - } - - MembersTabPanel { - model: root.bannedMembersModel - rootStore: root.rootStore - utilsStore: root.utilsStore - memberRole: root.memberRole - panelType: MembersTabPanel.TabType.BannedMembers - - Layout.fillWidth: true - Layout.fillHeight: true - - onUnbanUserClicked: root.unbanUserClicked(id) - onViewMemberMessagesClicked: root.viewMemberMessagesClicked(pubKey, displayName) + 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 1aa572c807..435cab7e09 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 + model: SortFilterProxyModel { + sourceModel: root.model + + sorters: [ + StringSorter { + roleName: "preferredDisplayName" + caseSensitivity: Qt.CaseInsensitive + } + ] + + filters: [ + UserFilterContainer { + searchString: root.searchString + } + ] } - StatusListView { - id: membersList - objectName: "CommunityMembersTabPanel_MembersListViews" + spacing: 0 - Layout.fillWidth: true - Layout.fillHeight: true + delegate: ContactListItemDelegate { + id: memberItem - model: SortFilterProxyModel { - id: filteredModel - sourceModel: root.model + // 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 - function searchPredicate(ensName, displayName, aliasName) { - const lowerCaseSearchString = memberSearch.text.toLowerCase() - const secondaryName = ProfileUtils.displayName("", ensName, displayName, aliasName) + /// Helpers /// - 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 { - 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 - } + // 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 + readonly property bool showOnHover: hovered && ctaAllowed + readonly property bool canDeleteMessages: model.isCurrentUser || model.memberRole !== Constants.memberRole.owner - delegate: StatusMemberListItem { - id: memberItem + /// 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 - // 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 + /// 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...") : "" - /// Helpers /// + isAwaitingAddress: model.membershipRequestState === Constants.CommunityMembershipRequestState.AwaitingAddress - // 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 + 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 { + 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 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 + readonly property string title: model.preferredDisplayName - /// 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...") : "" + width: ListView.view.width - isAwaitingAddress: model.membershipRequestState === Constants.CommunityMembershipRequestState.AwaitingAddress + icon.width: 40 + icon.height: 40 - rightPadding: 75 - leftPadding: 12 + onClicked: { + if (mouse.button === Qt.RightButton) { + const profileType = Utils.getProfileType(model.isCurrentUser, false, model.isBlocked) + const contactType = Utils.getContactType(model.contactRequest, model.isContact) - 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) + 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 } - ] - 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) - } + 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 3411830deb..020a773322 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 edc7e562e0..ece3a5fc6c 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 e75ca513be..990184282a 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 428f0d07d9..e77553f71c 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 0972906855..d8b0676016 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 - } + objectName: "ContactListPanel_ListView" - 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 + model: SortFilterProxyModel { + id: filteredModel - sourceModel: root.contactsModel + sourceModel: root.contactsModel - function panelUsagePredicate(isVerified) { - if (panelUsage === Constants.contactsPanelUsage.verifiedMutualContacts) - return isVerified - if (panelUsage === Constants.contactsPanelUsage.mutualContacts) - return !isVerified - - 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 + delegate: ContactPanel { + width: ListView.view.width - width: ListView.view.width - name: model.preferredDisplayName - iconSource: model.thumbnailImage + showSendMessageButton: model.isContact && !model.isBlocked + showRejectContactRequestButton: root.panelUsage === Constants.contactsPanelUsage.pendingContacts && + model.contactRequest === Constants.ContactRequestState.Received + showAcceptContactRequestButton: showRejectContactRequestButton - subTitle: model.ensVerified ? "" : Utils.getElidedCompressedPk(model.pubKey) - pubKeyColor: Utils.colorForPubkey(model.pubKey) - colorHash: Utils.getColorHashAsJson(model.pubKey, model.ensVerified) + contactText: root.panelUsage === Constants.contactsPanelUsage.pendingContacts && + model.contactRequest === Constants.ContactRequestState.Sent ? qsTr("Contact Request Sent") + : "" - showSendMessageButton: model.isContact && !model.isBlocked - showRejectContactRequestButton: { - if (root.panelUsage === Constants.contactsPanelUsage.receivedContactRequest - && !model.verificationRequestStatus) - return true - - return false - } - showAcceptContactRequestButton: { - if (root.panelUsage === Constants.contactsPanelUsage.receivedContactRequest - && !model.verificationRequestStatus) - return true - - return false - } - showRemoveRejectionButton: { - if (root.panelUsage === Constants.contactsPanelUsage.rejectedReceivedContactRequest) - return true - - return false - } - contactText: { - if (root.panelUsage === Constants.contactsPanelUsage.sentContactRequest) - return qsTr("Contact Request Sent") - - if (root.panelUsage === Constants.contactsPanelUsage.rejectedSentContactRequest) - return qsTr("Contact Request Rejected") - - return "" - } - - - 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 57d88abc5f..2e55cb8d02 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 b9c5666b62..e389307954 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 98340156cb..4847913aa9 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 38e3641aea..14b8c9f50c 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,11 +64,108 @@ SettingsContentBase { Global.openMenu(contactContextMenuComponent, this, params) } - Item { - id: contentItem + headerComponents: ColumnLayout { width: root.contentWidth - height: (searchBox.height + contactsTabBar.height - + stackLayout.height + (2 * Theme.bigPadding)) + spacing: Theme.padding + + StatusTabBar { + id: contactsTabBar + Layout.fillWidth: true + + StatusTabButton { + readonly property int panelUsage: Constants.contactsPanelUsage.mutualContacts + + width: implicitWidth + text: qsTr("Contacts") + } + StatusTabButton { + readonly property int panelUsage: Constants.contactsPanelUsage.pendingContacts + + objectName: "ContactsView_PendingRequest_Button" + width: implicitWidth + enabled: !!root.pendingContactsModel && !root.pendingContactsModel.ModelCount.empty + text: qsTr("Pending Requests") + badge.value: root.pendingReceivedContactsCount + } + StatusTabButton { + readonly property int panelUsage: Constants.contactsPanelUsage.blockedContacts + + objectName: "ContactsView_Blocked_Button" + width: implicitWidth + enabled: !!root.blockedContactsModel && !root.blockedContactsModel.ModelCount.empty + text: qsTr("Blocked") + } + } + + SearchBox { + id: searchBox + Layout.fillWidth: true + placeholderText: qsTr("Search by name or chat key") + } + } + + ContactsListPanel { + id: contactsListPanel + width: root.contentWidth + height: root.availableHeight + + panelUsage: contactsTabBar.currentItem.panelUsage + contactsModel: { + switch (panelUsage) { + case Constants.contactsPanelUsage.pendingContacts: + return root.pendingContactsModel + case Constants.contactsPanelUsage.blockedContacts: + return root.blockedContactsModel + case Constants.contactsPanelUsage.mutualContacts: + default: + return root.mutualContactsModel + } + } + section.property: { + switch (contactsListPanel.panelUsage) { + case Constants.contactsPanelUsage.pendingContacts: + return "contactRequest" + case Constants.contactsPanelUsage.blockedContacts: + return "" + case Constants.contactsPanelUsage.mutualContacts: + default: + return "isVerified" + } + } + 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 + + header: NoFriendsRectangle { + width: ListView.view.width + visible: ListView.view.count === 0 + inviteButtonVisible: searchBox.text === "" + } + + searchString: searchBox.text + onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey) + onSendMessageActionTriggered: root.contactsStore.joinPrivateChat(publicKey) + onContactRequestAccepted: root.contactsStore.acceptContactRequest(publicKey, "") + onContactRequestRejected: root.contactsStore.dismissContactRequest(publicKey, "") + + Component { + id: sendContactRequestComponent + SendContactRequestModal { + contactsStore: root.contactsStore + onClosed: destroy() + } + } Component { id: contactContextMenuComponent @@ -98,191 +192,27 @@ SettingsContentBase { onClosed: destroy() } } - SearchBox { - id: searchBox - anchors.left: parent.left - anchors.right: parent.right - placeholderText: qsTr("Search by a display name or chat key") - } + } - StatusTabBar { - id: contactsTabBar - anchors.left: parent.left - anchors.right: parent.right - anchors.top: searchBox.bottom - anchors.topMargin: Theme.padding + component SectionComponent: Rectangle { + required property string section + property alias text: sectionText.text - StatusTabButton { - id: contactsBtn - leftPadding: Theme.padding - width: implicitWidth - text: qsTr("Contacts") - } - StatusTabButton { - id: pendingRequestsBtn - objectName: "ContactsView_PendingRequest_Button" - width: implicitWidth - enabled: !root.pendingReceivedRequestContactsModel.ModelCount.empty || - !root.pendingSentRequestContactsModel.ModelCount.empty - text: qsTr("Pending Requests") - badge.value: root.pendingReceivedRequestContactsModel.ModelCount.count - } - StatusTabButton { - id: blockedBtn - objectName: "ContactsView_Blocked_Button" - width: implicitWidth - enabled: !root.blockedContactsModel.ModelCount.empty - text: qsTr("Blocked") - } - } + width: ListView.view.width + height: sectionText.implicitHeight + color: Theme.palette.statusListItem.backgroundColor - StackLayout { - id: stackLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.top: contactsTabBar.bottom - currentIndex: contactsTabBar.currentIndex - anchors.topMargin: Theme.padding - // CONTACTS - ColumnLayout { - Layout.fillWidth: true - Layout.minimumHeight: 0 - Layout.maximumHeight: (verifiedContacts.height + mutualContacts.height + noFriendsItem.height) - visible: (stackLayout.currentIndex === 0) - onVisibleChanged: { - if (visible) { - stackLayout.height = height+contactsTabBar.anchors.topMargin; - } - } - spacing: Theme.padding - ContactsListPanel { - id: verifiedContacts + StatusBaseText { + id: sectionText + width: parent.width + anchors.verticalCenter: parent.verticalCenter + topPadding: Theme.halfPadding + bottomPadding: Theme.halfPadding - Layout.fillWidth: true - title: qsTr("Trusted Contacts") - visible: !noFriendsItem.visible && count > 0 - contactsModel: root.mutualContactsModel - searchString: searchBox.text - onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey) - panelUsage: Constants.contactsPanelUsage.verifiedMutualContacts - onSendMessageActionTriggered: { - root.contactsStore.joinPrivateChat(publicKey) - } - } - - ContactsListPanel { - id: mutualContacts - - Layout.fillWidth: true - visible: !noFriendsItem.visible && count > 0 - title: qsTr("Contacts") - contactsModel: root.mutualContactsModel - searchString: searchBox.text - onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey) - panelUsage: Constants.contactsPanelUsage.mutualContacts - - onSendMessageActionTriggered: { - root.contactsStore.joinPrivateChat(publicKey) - } - } - - Item { - id: noFriendsItem - - Layout.fillWidth: true - Layout.preferredHeight: visible ? (root.contentHeight - (2*searchBox.height) - contactsTabBar.height - contactsTabBar.anchors.topMargin) : 0 - visible: root.mutualContactsModel.ModelCount.empty - - NoFriendsRectangle { - anchors.centerIn: parent - text: qsTr("You don't have any contacts yet") - } - } - } - - // PENDING REQUESTS - ColumnLayout { - Layout.fillWidth: true - Layout.minimumHeight: 0 - Layout.maximumHeight: (receivedRequests.height + sentRequests.height) - spacing: Theme.padding - visible: (stackLayout.currentIndex === 1) - onVisibleChanged: { - if (visible) { - stackLayout.height = height+contactsTabBar.anchors.topMargin; - } - } - ContactsListPanel { - id: receivedRequests - - objectName: "receivedRequests_ContactsListPanel" - Layout.fillWidth: true - title: qsTr("Received") - searchString: searchBox.text - visible: count > 0 - onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey) - contactsModel: root.pendingReceivedRequestContactsModel - panelUsage: Constants.contactsPanelUsage.receivedContactRequest - - onSendMessageActionTriggered: { - root.contactsStore.joinPrivateChat(publicKey) - } - - onContactRequestAccepted: { - root.contactsStore.acceptContactRequest(publicKey, "") - } - - onContactRequestRejected: { - root.contactsStore.dismissContactRequest(publicKey, "") - } - } - - ContactsListPanel { - id: sentRequests - - objectName: "sentRequests_ContactsListPanel" - Layout.fillWidth: true - title: qsTr("Sent") - searchString: searchBox.text - visible: count > 0 - onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey) - contactsModel: root.pendingSentRequestContactsModel - panelUsage: Constants.contactsPanelUsage.sentContactRequest - } - } - - // BLOCKED - ContactsListPanel { - id: blockedContacts - - Layout.fillWidth: true - searchString: searchBox.text - onOpenContactContextMenu: root.openContextMenu(contactsModel, publicKey) - contactsModel: root.blockedContactsModel - panelUsage: Constants.contactsPanelUsage.blockedContacts - visible: (stackLayout.currentIndex === 2) - onVisibleChanged: { - if (visible) { - stackLayout.height = height; - } - } - } - } - - Component { - id: loadingIndicator - StatusLoadingIndicator { - width: 12 - height: 12 - } - } - - Component { - id: sendContactRequest - SendContactRequestModal { - contactsStore: root.contactsStore - onClosed: destroy() - } + color: Theme.palette.baseColor1 + font.pixelSize: Theme.additionalTextSize + font.weight: Font.Medium + elide: Text.ElideRight } } } diff --git a/ui/app/AppLayouts/Profile/views/qmldir b/ui/app/AppLayouts/Profile/views/qmldir index ae9757614b..fcb0bd7d3f 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 b09f525202..9821e720d9 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 85a43ab040..7e433d6d05 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 0000000000..16e423f9e8 --- /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 9a34140d8f..d1ac919d95 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 3f7a69d726..1e2cf90077 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 3e56315405..b1ef3b17e6 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 f44a611dbc..2694af97c9 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 {