diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml index 40fd701b2c..f42a4ebc6f 100644 --- a/storybook/PagesModel.qml +++ b/storybook/PagesModel.qml @@ -145,6 +145,10 @@ ListModel { title: "HoldingsDropdown" section: "Popups" } + ListElement { + title: "MembersDropdown" + section: "Popups" + } ListElement { title: "InDropdown" section: "Popups" diff --git a/storybook/figma.json b/storybook/figma.json index 5762fdd005..720d5e7f31 100644 --- a/storybook/figma.json +++ b/storybook/figma.json @@ -24,6 +24,9 @@ "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=448%3A36296", "https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1573%3A296338" ], + "ChatAnchorButtonsPanel": [ + "https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/%F0%9F%92%AC-Chat%E2%8E%9CDesktop?node-id=14632-460085&t=SGTU2JeRA8ifbv2E-0" + ], "CommunitiesPortalLayout": [ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A415655", "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A415935" @@ -76,6 +79,9 @@ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A489607", "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2975%3A492910" ], + "DerivationPathInput": [ + "https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=12272%3A269692&t=YiipgcxOhdOvqprP-0" + ], "DidYouKnowSplashScreen": [ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba⎜Desktop?node-id=25878%3A518438&t=C7xTpNib38t7s7XU-4" ], @@ -118,6 +124,10 @@ "LoginView": [ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=1080%3A313192" ], + "MembersDropdown": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22647-498410", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22642-497015" + ], "NetworkSelectPopup": [ "https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=13200-352357&t=jKciSCy3BVlrZmBs-0", "https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=13185-350333&t=b2AclcJgxjXDL6Wl-0", @@ -172,11 +182,5 @@ "StatusCommunityCard": [ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A416159", "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=8159%3A416160" - ], - "ChatAnchorButtonsPanel": [ - "https://www.figma.com/file/Mr3rqxxgKJ2zMQ06UAKiWL/%F0%9F%92%AC-Chat%E2%8E%9CDesktop?node-id=14632-460085&t=SGTU2JeRA8ifbv2E-0" - ], - "DerivationPathInput": [ - "https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=12272%3A269692&t=YiipgcxOhdOvqprP-0" ] } diff --git a/storybook/pages/MembersDropdownPage.qml b/storybook/pages/MembersDropdownPage.qml new file mode 100644 index 0000000000..f37f041daa --- /dev/null +++ b/storybook/pages/MembersDropdownPage.qml @@ -0,0 +1,315 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml 2.15 + +import AppLayouts.Chat.controls.community 1.0 + +import utils 1.0 +import SortFilterProxyModel 0.2 + +import Storybook 1.0 + +SplitView { + id: root + + property bool globalUtilsReady: false + property bool mainModuleReady: false + + orientation: Qt.Vertical + + Logs { id: logs } + + QtObject { + function isCompressedPubKey(publicKey) { + return true + } + + function getColorId(publicKey) { + return Math.floor(Math.random() * 10) + } + + Component.onCompleted: { + Utils.globalUtilsInst = this + globalUtilsReady = true + + } + Component.onDestruction: { + globalUtilsReady = false + Utils.globalUtilsInst = {} + } + } + + QtObject { + function getContactDetailsAsJson() { + return JSON.stringify({ ensVerified: true }) + } + + Component.onCompleted: { + mainModuleReady = true + Utils.mainModuleInst = this + } + Component.onDestruction: { + mainModuleReady = false + Utils.mainModuleInst = {} + } + } + + + ListModel { + id: members + + property int counter: 0 + + function addMember() { + const i = counter++ + const key = `pub_key_${i}` + + const firstLetters = ["a", "b", "c", "d"] + const firstLetterIdx = Math.min(Math.floor(i / firstLetters.length), + firstLetters.length - 1) + const firstLetter = firstLetters[firstLetterIdx] + + append({ + alias: "", + colorId: "1", + displayName: `${firstLetter}contact ${i}`, + ensName: "", + icon: "", + isContact: true, + localNickname: "", + onlineStatus: 1, + pubKey: key, + isVerified: true, + isUntrustworthy: false + }) + } + + Component.onCompleted: { + for (let i = 0; i < 33; i++) + addMember() + } + } + + Pane { + id: container + + SplitView.fillWidth: true + SplitView.fillHeight: true + + Rectangle { + id: startRect + + border.color: "green" + color: "lightgreen" + border.width: 3 + width: 50 + height: width + + x: 70 + y: 70 + + radius: width / 2 + + Drag.active: dragArea.drag.active + + MouseArea { + id: dragArea + + anchors.fill: parent + drag.target: parent + } + } + + Loader { + id: loader + + anchors.centerIn: parent + active: globalUtilsReady && mainModuleReady + + sourceComponent: MembersDropdown { + id: membersDropdown + + closePolicy: Popup.NoAutoClose + + model: SortFilterProxyModel { + Binding on sourceModel { + when: globalUtilsReady && mainModuleReady + value: members + restoreMode: Binding.RestoreBindingOrValue + } + + filters: [ + ExpressionFilter { + enabled: membersDropdown.searchText !== "" + + function matchesAlias(name, filter) { + return name.split(" ").some(p => p.startsWith(filter)) + } + + expression: { + membersDropdown.selectedKeys + membersDropdown.searchText + + if (membersDropdown.selectedKeys.indexOf(model.pubKey) > -1) + return true + + const filter = membersDropdown.searchText.toLowerCase() + return matchesAlias(model.alias.toLowerCase(), filter) + || model.displayName.toLowerCase().includes(filter) + || model.ensName.toLowerCase().includes(filter) + || model.localNickname.toLowerCase().includes(filter) + || model.pubKey.toLowerCase().includes(filter) + } + } + ] + } + + onBackButtonClicked: { + logs.logEvent("MembersDropdown::backButtonClicked") + } + + onAddButtonClicked: { + logs.logEvent("MembersDropdown::addButtonClicked, keys: " + + membersDropdown.selectedKeys) + } + + Component.onCompleted: open() + } + } + } + + LogsAndControlsPanel { + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 250 + + logsView.logText: logs.logText + + Loader { + active: loader.item + + anchors.left: parent.left + anchors.right: parent.right + + sourceComponent: ColumnLayout { + readonly property MembersDropdown membersDropdown: loader.item + + RowLayout { + Label { + text: "maximum list height:" + } + + Slider { + id: maxListHeightSlider + from: 100 + to: 500 + stepSize: 1 + + Component.onCompleted: { + value = membersDropdown.maximumListHeight + membersDropdown.maximumListHeight + = Qt.binding(() => value) + } + } + + Label { + text: maxListHeightSlider.value + } + } + + RowLayout { + Label { + text: "margins:" + } + + Slider { + id: marginsSlider + from: -1 + to: 50 + stepSize: 1 + + Component.onCompleted: { + value = membersDropdown.margins + membersDropdown.margins = Qt.binding(() => value) + } + } + + Label { + text: marginsSlider.value + } + } + + RowLayout { + Label { + text: "bottom inset:" + } + + Slider { + id: bottomInsetSlider + from: 0 + to: 50 + stepSize: 1 + + Component.onCompleted: { + value = membersDropdown.bottomInset + membersDropdown.bottomInset = Qt.binding(() => value) + } + } + + Label { + text: bottomInsetSlider.value + } + } + + RowLayout { + RadioButton { + id: anchorToItemRadioButton + text: "anchor to item" + + checked: true + } + RadioButton { + id: anchorToOverlayRadioButton + text: "anchor to overlay" + + } + + Binding { + target: membersDropdown + property: "parent" + value: anchorToItemRadioButton.checked + ? startRect : membersDropdown.Overlay.overlay + } + + Binding { + target: membersDropdown.anchors + when: anchorToOverlayRadioButton.checked + property: "centerIn" + value: membersDropdown.parent + restoreMode: Binding.RestoreBindingOrValue + } + + Binding { + target: membersDropdown + property: "x" + value: anchorToItemRadioButton.checked + ? startRect.width / 2 : 0 + } + + Binding { + target: membersDropdown + property: "y" + value: anchorToItemRadioButton.checked + ? startRect.height / 2 : 0 + } + } + + Label { + Layout.fillWidth: true + text: `selected members: ${membersDropdown.selectedKeys}` + wrapMode: Label.Wrap + } + } + } + } +} diff --git a/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml b/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml index 62ded206db..906724269c 100644 --- a/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml +++ b/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml @@ -166,10 +166,9 @@ StatusDropdown { visible: statesStack.size > 1 spacing: 0 leftPadding: 4 - statusIcon: "next" + statusIcon: "previous" icon.width: 12 icon.height: 12 - iconRotation: 180 text: qsTr("Back") } diff --git a/ui/app/AppLayouts/Chat/controls/community/MembersDropdown.qml b/ui/app/AppLayouts/Chat/controls/community/MembersDropdown.qml new file mode 100644 index 0000000000..589f748ab5 --- /dev/null +++ b/ui/app/AppLayouts/Chat/controls/community/MembersDropdown.qml @@ -0,0 +1,234 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core.Utils 0.1 + +import shared.controls 1.0 +import shared.controls.delegates 1.0 + +StatusDropdown { + id: root + + property var selectedKeys: [] + property int maximumListHeight: 288 + + property alias model: listView.model + readonly property alias count: listView.count + + readonly property alias searchText: filterInput.text + + property bool fixedYPosition: !anchors.centerIn && margins < 0 + + signal backButtonClicked + signal addButtonClicked + + width: 295 + + height: Math.min( + d.availableExternalHeight, + content.requestedHeight + d.vPadding) + + padding: 11 + bottomInset: 10 + bottomPadding: padding + bottomInset + + QtObject { + id: d + + readonly property int sectionDelegateHeight: 40 + readonly property int delegateHeight: 47 + + readonly property int vPadding: root.topPadding + root.bottomPadding + + readonly property int availableExternalHeight: + (root.Overlay.overlay ? root.Overlay.overlay.height : 0) - root.bottomMargin - + (root.fixedYPosition ? contentItem.parent.y : root.topMargin) + } + + contentItem: ColumnLayout { + id: content + + spacing: 8 + height: root.availableHeight + clip: true + + readonly property int requestedHeight: + backButton.height + + spacing + filterInput.height + + spacing + (listView.count + ? Math.min(listView.contentHeight, root.maximumListHeight) + : noContactsText.Layout.preferredHeight) + + spacing + addButton.height + + StatusIconTextButton { + id: backButton + + Layout.preferredHeight: 48 + Layout.maximumWidth: root.availableWidth + + spacing: 0 + leftPadding: 4 + statusIcon: "previous" + icon.width: 12 + icon.height: 12 + text: qsTr("Back") + + onClicked: root.backButtonClicked() + } + + SearchBox { + id: filterInput + + Layout.fillWidth: true + + placeholderText: qsTr("Search members") + maximumHeight: 36 + topPadding: 0 + bottomPadding: 0 + + input.asset.width: 15 + input.asset.height: 15 + input.leftPadding: 13 + input.font.pixelSize: 13 + input.placeholder.font.pixelSize: 13 + } + + StatusBaseText { + id: noContactsText + + Layout.fillWidth: true + Layout.preferredHeight: 50 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + visible: listView.count === 0 + + text: qsTr("No contacts found") + color: Theme.palette.baseColor1 + font.pixelSize: Theme.tertiaryTextFontSize + elide: Text.ElideRight + lineHeight: 1.2 + } + + StatusListView { + id: listView + + Layout.fillWidth: true + Layout.fillHeight: true + + visible: count > 0 + + header: StatusCheckBox { + width: ListView.view.width + + text: qsTr("Select all") + font.weight: Font.Medium + + checked: root.selectedKeys.length === listView.count + + leftSide: false + size: StatusCheckBox.Size.Small + indicator.anchors.rightMargin: 12 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + + onClicked: { + if (listView.headerItem.checked) { + root.selectedKeys = [] + return + } + + const count = root.model.rowCount() + const keys = [] + + for (let i = 0; i < count; i++) { + const key = ModelUtils.get(root.model, i, "pubKey") + keys.push(key) + } + + root.selectedKeys = keys + } + } + } + + delegate: ContactListItemDelegate { + id: delegateRoot + + width: ListView.view.width + height: d.delegateHeight + asset.width: 29 + asset.height: 29 + + rightPadding: 0 + leftPadding: 6 + + color: "transparent" + + onClicked: { + const index = root.selectedKeys.indexOf(model.pubKey) + const selectedKeysCopy = Object.assign( + [], root.selectedKeys) + + if (index === -1) + selectedKeysCopy.push(model.pubKey) + else + selectedKeysCopy.splice(index, 1) + + root.selectedKeys = selectedKeysCopy + } + + components: [ + StatusCheckBox { + id: contactCheckbox + + size: StatusCheckBox.Size.Small + checked: root.selectedKeys.indexOf(model.pubKey) > -1 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + + onClicked: delegateRoot.clicked( + delegateRoot.itemId, mouse) + } + } + ] + } + + section.property: "displayName" + section.criteria: ViewSection.FirstCharacter + section.delegate: StatusBaseText { + text: section.toUpperCase() + + width: ListView.view.width + height: d.sectionDelegateHeight + + padding: 5 + verticalAlignment: Qt.AlignVCenter + + color: Theme.palette.baseColor1 + font.pixelSize: Theme.tertiaryTextFontSize + } + } + + StatusButton { + id: addButton + + Layout.fillWidth: true + + textFillWidth: true + enabled: root.selectedKeys.length > 0 + text: enabled + ? qsTr("Add %n member(s)", "", root.selectedKeys.length) + : qsTr("Add") + + onClicked: root.addButtonClicked() + } + } +} diff --git a/ui/app/AppLayouts/Chat/controls/community/qmldir b/ui/app/AppLayouts/Chat/controls/community/qmldir index c05ad909f5..5e2aa460af 100644 --- a/ui/app/AppLayouts/Chat/controls/community/qmldir +++ b/ui/app/AppLayouts/Chat/controls/community/qmldir @@ -8,6 +8,7 @@ HoldingTypes 1.0 HoldingTypes.qml HoldingsDropdown 1.0 HoldingsDropdown.qml InDropdown 1.0 InDropdown.qml InlineNetworksComboBox 1.0 InlineNetworksComboBox.qml +MembersDropdown 1.0 MembersDropdown.qml MembersSelectorPanel 1.0 MembersSelectorPanel.qml PermissionItem 1.0 PermissionItem.qml PermissionsDropdown 1.0 PermissionsDropdown.qml diff --git a/ui/imports/shared/views/PickedContacts.qml b/ui/imports/shared/views/PickedContacts.qml index 168ad28a77..2a1660b450 100644 --- a/ui/imports/shared/views/PickedContacts.qml +++ b/ui/imports/shared/views/PickedContacts.qml @@ -23,13 +23,6 @@ Item { readonly property alias count: contactGridView.count - signal contactClicked(var contact) - - function matchesAlias(name, filter) { - let parts = name.split(" ") - return parts.some(p => p.startsWith(filter)) - } - StatusGridView { id: contactGridView anchors.fill: parent