From 27aac8d83a5f0eadf73175c429ae1b2ebf969b6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Tue, 25 Apr 2023 23:24:04 +0200 Subject: [PATCH] feat(CommunityNewAirdropView): design-compliant token airdrop flow for collectibles --- storybook/PagesModel.qml | 4 + storybook/figma.json | 8 + .../pages/AirdropRecipientsSelectorPage.qml | 11 +- .../CommunityAirdropsSettingsPanelPage.qml | 1 + .../pages/CommunityNewAirdropViewPage.qml | 180 +++++++++++ storybook/pages/MembersDropdownPage.qml | 4 + .../StatusQ/Controls/StatusIconTextButton.qml | 1 + .../controls/community/AddressesInputList.qml | 3 +- .../community/AirdropRecipientsSelector.qml | 18 +- .../community/AirdropTokensSelector.qml | 9 +- .../controls/community/MembersDropdown.qml | 2 + .../Chat/controls/community/TokenItem.qml | 2 +- .../CommunityAirdropsSettingsPanel.qml | 7 +- .../Chat/views/CommunitySettingsView.qml | 47 ++- .../communities/CommunityNewAirdropView.qml | 305 ++++++++++++------ .../shared/stores/CommunityTokensStore.qml | 6 +- 16 files changed, 482 insertions(+), 126 deletions(-) create mode 100644 storybook/pages/CommunityNewAirdropViewPage.qml diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml index b7cfd15f1b..4e97a291ce 100644 --- a/storybook/PagesModel.qml +++ b/storybook/PagesModel.qml @@ -17,6 +17,10 @@ ListModel { title: "CommunityNewPermissionView" section: "Views" } + ListElement { + title: "CommunityNewAirdropView" + section: "Views" + } ListElement { title: "ProfileFetchingView" section: "Views" diff --git a/storybook/figma.json b/storybook/figma.json index 7d3c526471..bfab619713 100644 --- a/storybook/figma.json +++ b/storybook/figma.json @@ -45,6 +45,14 @@ "CommunityMintedTokensView": [ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2934%3A479136&t=zs22ORYUVDYpqubQ-1" ], + "CommunityNewAirdropView": [ + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22602-495563&t=9dIP8Sji2UlfhsEs-0", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-495258&t=9dIP8Sji2UlfhsEs-0", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-496145&t=9dIP8Sji2UlfhsEs-0", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22647-497754&t=9dIP8Sji2UlfhsEs-0", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22647-501014&t=9dIP8Sji2UlfhsEs-0", + "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22647-499051&t=kHAcE8WSCyGqhWSH-0" + ], "CommunityNewCollectibleView": [ "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=2934%3A480877&t=Qo2FwPRxvSxbluqB-1", "https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=26601%3A518245&t=Qo2FwPRxvSxbluqB-1", diff --git a/storybook/pages/AirdropRecipientsSelectorPage.qml b/storybook/pages/AirdropRecipientsSelectorPage.qml index cb42269b64..bf6d09f30f 100644 --- a/storybook/pages/AirdropRecipientsSelectorPage.qml +++ b/storybook/pages/AirdropRecipientsSelectorPage.qml @@ -110,11 +110,10 @@ SplitView { showAddressesInputWhenEmpty: showAddressesInputWhenEmptyCheckBox.checked - infiniteExpectedNumberOfRecipients: - infiniteExpectedNumberOfRecipientsCheckBox.checked + infiniteMaxNumberOfRecipients: + infiniteMaxNumberOfRecipientsCheckBox.checked - expectedNumberOfRecipients: - expectedNumberOfRecipientsSpinBox.value + maxNumberOfRecipients: maxNumberOfRecipientsSpinBox.value onAddAddressesRequested: timer.start() onRemoveAddressRequested: addresses.remove(index) @@ -163,7 +162,7 @@ SplitView { } CheckBox { - id: infiniteExpectedNumberOfRecipientsCheckBox + id: infiniteMaxNumberOfRecipientsCheckBox text: "Infinite number of expected recipients" } @@ -175,7 +174,7 @@ SplitView { } SpinBox { - id: expectedNumberOfRecipientsSpinBox + id: maxNumberOfRecipientsSpinBox value: 2 from: 1 diff --git a/storybook/pages/CommunityAirdropsSettingsPanelPage.qml b/storybook/pages/CommunityAirdropsSettingsPanelPage.qml index b2dd5e5ada..26e3a2b82f 100644 --- a/storybook/pages/CommunityAirdropsSettingsPanelPage.qml +++ b/storybook/pages/CommunityAirdropsSettingsPanelPage.qml @@ -25,6 +25,7 @@ SplitView { anchors.topMargin: 50 assetsModel: AssetsModel {} collectiblesModel: CollectiblesModel {} + membersModel: ListModel {} onAirdropClicked: logs.logEvent("CommunityAirdropsSettingsPanel::onAirdropClicked") } diff --git a/storybook/pages/CommunityNewAirdropViewPage.qml b/storybook/pages/CommunityNewAirdropViewPage.qml new file mode 100644 index 0000000000..dd2a7929ad --- /dev/null +++ b/storybook/pages/CommunityNewAirdropViewPage.qml @@ -0,0 +1,180 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import AppLayouts.Chat.views.communities 1.0 +import AppLayouts.Chat.controls.community 1.0 + +import Storybook 1.0 +import Models 1.0 + +import SortFilterProxyModel 0.2 +import utils 1.0 + +SplitView { + orientation: Qt.Vertical + SplitView.fillWidth: true + + property bool globalUtilsReady: false + property bool mainModuleReady: false + + Logs { id: logs } + + QtObject { + function isCompressedPubKey(publicKey) { + return true + } + + function getCompressedPk(publicKey) { + return "compressed_" + publicKey + } + + 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 { + SplitView.fillWidth: true + SplitView.fillHeight: true + + Loader { + anchors.fill: parent + active: globalUtilsReady && mainModuleReady + + sourceComponent: CommunityNewAirdropView { + id: communityNewPermissionView + + CollectiblesModel { + id: collectiblesModel + } + + SortFilterProxyModel { + id: collectiblesModelWithSupply + + sourceModel: collectiblesModel + + proxyRoles: [ + ExpressionRole { + name: "supply" + expression: ((model.index + 1) * 115).toString() + }, + ExpressionRole { + name: "infiniteSupply" + expression: !(model.index % 4) + }, + ExpressionRole { + name: "chainName" + expression: model.index ? "Optimism" : "Arbitrum" + }, + ExpressionRole { + + readonly property string icon1: "network/Network=Optimism" + readonly property string icon2: "network/Network=Arbitrum" + + name: "chainIcon" + expression: model.index ? icon1 : icon2 + } + ] + + filters: ValueFilter { + roleName: "category" + value: TokenCategories.Category.Community + } + + + Component.onCompleted: { + Qt.callLater(() => communityNewPermissionView.collectiblesModel = this) + } + } + + assetsModel: ListModel {} + collectiblesModel: ListModel {} + membersModel: members + + onAirdropClicked: { + logs.logEvent("CommunityNewAirdropView::airdropClicked", ["airdropTokens", "addresses", "membersPubKeys"], arguments) + } + } + } + } + + LogsAndControlsPanel { + id: logsAndControlsPanel + + SplitView.minimumHeight: 100 + SplitView.preferredHeight: 160 + + logsView.logText: logs.logText + + ColumnLayout { + MenuSeparator {} + + TextEdit { + readOnly: true + selectByMouse: true + text: "valid address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4" + } + } + } +} diff --git a/storybook/pages/MembersDropdownPage.qml b/storybook/pages/MembersDropdownPage.qml index f37f041daa..9ec4e5978a 100644 --- a/storybook/pages/MembersDropdownPage.qml +++ b/storybook/pages/MembersDropdownPage.qml @@ -25,6 +25,10 @@ SplitView { return true } + function getCompressedPk(publicKey) { + return "compressed_" + publicKey + } + function getColorId(publicKey) { return Math.floor(Math.random() * 10) } diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusIconTextButton.qml b/ui/StatusQ/src/StatusQ/Controls/StatusIconTextButton.qml index 96c106e533..5c2cdee982 100644 --- a/ui/StatusQ/src/StatusQ/Controls/StatusIconTextButton.qml +++ b/ui/StatusQ/src/StatusQ/Controls/StatusIconTextButton.qml @@ -68,6 +68,7 @@ AbstractButton { } StatusBaseText { Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true text: root.text color: root.textColor font.pixelSize: root.font.pixelSize diff --git a/ui/app/AppLayouts/Chat/controls/community/AddressesInputList.qml b/ui/app/AppLayouts/Chat/controls/community/AddressesInputList.qml index 78f6ca7103..dea012a630 100644 --- a/ui/app/AppLayouts/Chat/controls/community/AddressesInputList.qml +++ b/ui/app/AppLayouts/Chat/controls/community/AddressesInputList.qml @@ -180,10 +180,11 @@ Control { multiline: true topPadding: bottomPadding + (listView.count ? d.spacing : 0) + bottomPadding: 5 height: edit.implicitHeight + topPadding + bottomPadding - placeholderText: qsTr("Example: 0x39cf...fbd2") + placeholderText: root.count ? "" : qsTr("Example: 0x39cf...fbd2") Keys.onPressed: { if ((event.key !== Qt.Key_Return && event.key !== Qt.Key_Enter) diff --git a/ui/app/AppLayouts/Chat/controls/community/AirdropRecipientsSelector.qml b/ui/app/AppLayouts/Chat/controls/community/AirdropRecipientsSelector.qml index 4b60e47bb4..5616721857 100644 --- a/ui/app/AppLayouts/Chat/controls/community/AirdropRecipientsSelector.qml +++ b/ui/app/AppLayouts/Chat/controls/community/AirdropRecipientsSelector.qml @@ -17,14 +17,15 @@ StatusFlowSelector { property alias addressesInputText: addressesSelectorPanel.text property bool showAddressesInputWhenEmpty: false - property int expectedNumberOfRecipients: 0 - property bool infiniteExpectedNumberOfRecipients: false + property int maxNumberOfRecipients: 0 + property bool infiniteMaxNumberOfRecipients: false readonly property int count: addressesSelectorPanel.count + membersSelectorPanel.count readonly property bool valid: - addressesSelectorPanel.invalidAddressesCount === 0 + addressesSelectorPanel.invalidAddressesCount === 0 && + (infiniteMaxNumberOfRecipients || count <= maxNumberOfRecipients) signal addAddressesRequested(string addresses) signal removeAddressRequested(int index) @@ -35,7 +36,8 @@ StatusFlowSelector { title: qsTr("To") icon: Style.svg("member") - flowSpacing: 12 + flowSpacing: addressesSelectorPanel.visible || membersSelectorPanel.visible + ? 12 : 6 placeholderText: qsTr("Example: 12 addresses and 3 members") @@ -61,12 +63,12 @@ StatusFlowSelector { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - readonly property bool valid: root.infiniteExpectedNumberOfRecipients || - root.count <= root.expectedNumberOfRecipients + readonly property bool valid: root.infiniteMaxNumberOfRecipients || + root.count <= root.maxNumberOfRecipients - text: root.count + " / " + (root.infiniteExpectedNumberOfRecipients + text: root.count + " / " + (root.infiniteMaxNumberOfRecipients ? qsTr("∞ recipients", "infinite number of recipients") - : qsTr("%n recipient(s)", "", root.expectedNumberOfRecipients)) + : qsTr("%n recipient(s)", "", root.maxNumberOfRecipients)) font.pixelSize: Theme.tertiaryTextFontSize + 1 color: valid ? Theme.palette.baseColor1 : Theme.palette.dangerColor1 diff --git a/ui/app/AppLayouts/Chat/controls/community/AirdropTokensSelector.qml b/ui/app/AppLayouts/Chat/controls/community/AirdropTokensSelector.qml index efd66be3b6..b3588a8196 100644 --- a/ui/app/AppLayouts/Chat/controls/community/AirdropTokensSelector.qml +++ b/ui/app/AppLayouts/Chat/controls/community/AirdropTokensSelector.qml @@ -12,8 +12,9 @@ StatusFlowSelector { id: root property alias model: repeater.model + readonly property alias count: repeater.count - signal itemClicked(int index, var mouse) + signal itemClicked(int index, var mouse, var item) placeholderText: qsTr("Example: 1 SOCK") placeholderItem.visible: repeater.count === 0 @@ -32,9 +33,13 @@ StatusFlowSelector { id: repeater Control { + id: delegateRoot + component Icon: StatusRoundedImage { implicitWidth: d.iconSize implicitHeight: d.iconSize + + image.mipmap: true } component Text: StatusBaseText { @@ -55,7 +60,7 @@ StatusFlowSelector { MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor - onClicked: root.itemClicked(model.index, mouse) + onClicked: root.itemClicked(model.index, mouse, delegateRoot) } } diff --git a/ui/app/AppLayouts/Chat/controls/community/MembersDropdown.qml b/ui/app/AppLayouts/Chat/controls/community/MembersDropdown.qml index 589f748ab5..3faec87dfa 100644 --- a/ui/app/AppLayouts/Chat/controls/community/MembersDropdown.qml +++ b/ui/app/AppLayouts/Chat/controls/community/MembersDropdown.qml @@ -36,6 +36,8 @@ StatusDropdown { bottomInset: 10 bottomPadding: padding + bottomInset + onOpened: filterInput.text = "" + QtObject { id: d diff --git a/ui/app/AppLayouts/Chat/controls/community/TokenItem.qml b/ui/app/AppLayouts/Chat/controls/community/TokenItem.qml index dc5199d105..ebcb8dc1c7 100644 --- a/ui/app/AppLayouts/Chat/controls/community/TokenItem.qml +++ b/ui/app/AppLayouts/Chat/controls/community/TokenItem.qml @@ -68,7 +68,7 @@ Control { } StatusBaseText { - visible: !!root.amount + visible: !!root.amount && !root.selected text: root.amount color: Theme.palette.baseColor1 font.pixelSize: 12 diff --git a/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml b/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml index aadd14b728..8f62ff05da 100644 --- a/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml +++ b/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml @@ -17,9 +17,11 @@ SettingsPageLayout { required property var assetsModel required property var collectiblesModel + required property var membersModel + property int viewWidth: 560 // by design - signal airdropClicked(var airdropTokens, var addresses) + signal airdropClicked(var airdropTokens, var addresses, var membersPubKeys) signal navigateToMintTokenSettings @@ -102,9 +104,10 @@ SettingsPageLayout { assetsModel: root.assetsModel collectiblesModel: root.collectiblesModel + membersModel: root.membersModel onAirdropClicked: { - root.airdropClicked(airdropTokens, addresses) + root.airdropClicked(airdropTokens, addresses, membersPubKeys) stackManager.clear(d.welcomeViewState, StackView.Immediate) } onNavigateToMintTokenSettings: root.navigateToMintTokenSettings() diff --git a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml index a068fa790b..2a984c9249 100644 --- a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml @@ -19,6 +19,7 @@ import StatusQ.Controls 0.1 import StatusQ.Controls.Validators 0.1 import AppLayouts.Chat.stores 1.0 +import AppLayouts.Chat.controls.community 1.0 import shared.stores 1.0 import shared.views.chat 1.0 @@ -370,8 +371,48 @@ StatusSectionLayout { readonly property CommunityTokensStore communityTokensStore: rootStore.communityTokensStore - assetsModel: rootStore.assetsModel - collectiblesModel: rootStore.collectiblesModel + assetsModel: ListModel {} + + readonly property var communityTokens: root.community.communityTokens + + Loader { + id: modelLoader + active: airdropPanel.communityTokens + + sourceComponent: SortFilterProxyModel { + + sourceModel: airdropPanel.communityTokens + + proxyRoles: [ + ExpressionRole { + name: "category" + + // Singleton cannot be used directly in the epression + readonly property int category: TokenCategories.Category.Own + expression: category + }, + ExpressionRole { + name: "iconSource" + expression: model.image + }, + ExpressionRole { + name: "key" + expression: model.symbol + } + ] + } + } + + collectiblesModel: modelLoader.item + + membersModel: { + const chatContentModule = root.rootStore.currentChatContentModule() + if (!chatContentModule || !chatContentModule.usersModule) { + // New communities have no chats, so no chatContentModule + return null + } + return chatContentModule.usersModule.model + } onPreviousPageNameChanged: root.backButtonName = previousPageName onAirdropClicked: communityTokensStore.airdrop(root.community.id, airdropTokens, addresses) @@ -413,7 +454,7 @@ StatusSectionLayout { if(d.currentItem && d.currentItem.goTo) { d.currentItem.goTo(subSection) } - } + } } } diff --git a/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml b/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml index e18de5d3ad..561d09cca9 100644 --- a/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml +++ b/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml @@ -1,10 +1,8 @@ -import QtQuick 2.14 -import QtQuick.Controls 2.14 -import QtQuick.Layouts 1.14 +import QtQuick 2.15 +import QtQuick.Layouts 1.15 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 -import StatusQ.Components 0.1 import StatusQ.Controls 0.1 import StatusQ.Core.Utils 0.1 @@ -15,7 +13,9 @@ import AppLayouts.Chat.helpers 1.0 import AppLayouts.Chat.panels.communities 1.0 import AppLayouts.Chat.controls.community 1.0 -// TEMPORAL - BASIC IMPLEMENTATION +import SortFilterProxyModel 0.2 + + StatusScrollView { id: root @@ -23,21 +23,27 @@ StatusScrollView { required property var assetsModel required property var collectiblesModel + // Community members model: + required property var membersModel + property int viewWidth: 560 // by design - // roles: type, key, name, amount, imageSource - property var selectedHoldingsModel: ListModel {} + readonly property var selectedHoldingsModel: ListModel {} - readonly property bool isFullyFilled: selectedHoldingsModel.count > 0 && - addressess.model.count > 0 + readonly property bool isFullyFilled: tokensSelector.count > 0 && + airdropRecipientsSelector.count > 0 && + airdropRecipientsSelector.valid - signal airdropClicked(var airdropTokens, var addresses) + signal airdropClicked(var airdropTokens, var addresses, var membersPubKeys) signal navigateToMintTokenSettings function selectCollectible(key, amount) { const modelItem = CommunityPermissionsHelpers.getTokenByKey( root.collectiblesModel, key) - d.addItem(HoldingTypes.Type.Collectible, modelItem, amount) + + const entry = d.prepareEntry(key, amount) + entry.valid = true + selectedHoldingsModel.append(entry) } QtObject { @@ -47,22 +53,68 @@ StatusScrollView { readonly property int dropdownHorizontalOffset: 4 readonly property int dropdownVerticalOffset: 1 - function addItem(type, item, amount) { - const key = item.key + function prepareEntry(key, amount) { + const modelItem = CommunityPermissionsHelpers.getTokenByKey( + root.collectiblesModel, key) - root.selectedHoldingsModel.append({ type, key, amount }) + return { + key, amount, + tokenText: amount + " " + modelItem.name, + tokenImage: modelItem.iconSource, + networkText: modelItem.chainName, + networkImage: Style.svg(modelItem.chainIcon), + supply: modelItem.supply, + infiniteSupply: modelItem.infiniteSupply + } } } + Instantiator { + id: recipientsCountInstantiator + + model: selectedHoldingsModel + + property bool infinity: true + property int maximumRecipientsCount + + function findRecipientsCount() { + let min = Number.MAX_SAFE_INTEGER + + for (let i = 0; i < count; i++) { + const item = objectAt(i) + + if (!item || item.infiniteSupply) + continue + + min = Math.min(item.supply / item.amount, min) + } + + infinity = min === Number.MAX_SAFE_INTEGER + maximumRecipientsCount = infinity ? 0 : min + } + + delegate: QtObject { + readonly property int supply: model.supply + readonly property real amount: model.amount + readonly property bool infiniteSupply: model.infiniteSupply + + onSupplyChanged: recipientsCountInstantiator.findRecipientsCount() + onAmountChanged: recipientsCountInstantiator.findRecipientsCount() + onInfiniteSupplyChanged: recipientsCountInstantiator.findRecipientsCount() + } + + onCountChanged: findRecipientsCount() + } + contentWidth: mainLayout.width contentHeight: mainLayout.height - ColumnLayout { + SequenceColumnLayout { id: mainLayout width: root.viewWidth spacing: 0 - StatusItemSelector { + AirdropTokensSelector { id: tokensSelector property int editedIndex: -1 @@ -71,19 +123,10 @@ StatusScrollView { icon: Style.svg("token") title: qsTr("What") placeholderText: qsTr("Example: 1 SOCK") - tagLeftPadding: 2 - asset.height: 28 - asset.width: asset.height addButton.visible: model.count < d.maxAirdropTokens - model: HoldingsSelectionModel { - sourceModel: root.selectedHoldingsModel + model: root.selectedHoldingsModel - assetsModel: root.assetsModel - collectiblesModel: root.collectiblesModel - } - - // TODO: All this code is repeated inside `CommunityNewPermissionView`. Check how to reuse it. HoldingsDropdown { id: dropdown @@ -114,37 +157,26 @@ StatusScrollView { return itemIndex } - onAddAsset: { - const modelItem = CommunityPermissionsHelpers.getTokenByKey( - root.assetsModel, key) - d.addItem(HoldingTypes.Type.Asset, modelItem, amount) - dropdown.close() + onOpened: { + usedTokens = ModelUtils.modelToArray( + root.selectedHoldingsModel, ["key", "amount"]) } onAddCollectible: { - const modelItem = CommunityPermissionsHelpers.getTokenByKey( - root.collectiblesModel, key) - d.addItem(HoldingTypes.Type.Collectible, modelItem, amount) - dropdown.close() - } + const entry = d.prepareEntry(key, amount) - onUpdateAsset: { - const itemIndex = prepareUpdateIndex(key) - const modelItem = CommunityPermissionsHelpers.getTokenByKey(root.assetsModel, key) - - root.selectedHoldingsModel.set( - itemIndex, { type: HoldingTypes.Type.Asset, key, amount }) + selectedHoldingsModel.append(entry) dropdown.close() } onUpdateCollectible: { const itemIndex = prepareUpdateIndex(key) + + const entry = d.prepareEntry(key, amount) const modelItem = CommunityPermissionsHelpers.getTokenByKey( root.collectiblesModel, key) - root.selectedHoldingsModel.set( - itemIndex, - { type: HoldingTypes.Type.Collectible, key, amount }) + root.selectedHoldingsModel.set(itemIndex, entry) dropdown.close() } @@ -176,70 +208,146 @@ StatusScrollView { dropdown.x = mouse.x + d.dropdownHorizontalOffset dropdown.y = d.dropdownVerticalOffset - const modelItem = tokensSelector.model.get(index) - - switch(modelItem.type) { - case HoldingTypes.Type.Asset: - dropdown.assetKey = modelItem.key - dropdown.assetAmount = modelItem.amount - break - case HoldingTypes.Type.Collectible: - dropdown.collectibleKey = modelItem.key - dropdown.collectibleAmount = modelItem.amount - break - default: - console.warn("Unsupported holdings type.") - } - - dropdown.setActiveTab(modelItem.type) + const modelItem = selectedHoldingsModel.get(index) + dropdown.collectibleKey = modelItem.key + dropdown.collectibleAmount = modelItem.amount + dropdown.setActiveTab(HoldingTypes.Type.Collectible) dropdown.openUpdateFlow() editedIndex = index } } - Rectangle { - Layout.leftMargin: 16 - Layout.preferredWidth: 2 - Layout.preferredHeight: 24 - color: Style.current.separator - } + SequenceColumnLayout.Separator {} - // TEMPORAL - StatusInput { - id: addressInput + AirdropRecipientsSelector { + id: airdropRecipientsSelector - Layout.fillWidth: true + addressesModel: addresses - placeholderText: qsTr("Example: 0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7999") - } + infiniteMaxNumberOfRecipients: + recipientsCountInstantiator.infinity - Rectangle { - Layout.leftMargin: 16 - Layout.preferredWidth: 2 - Layout.preferredHeight: 24 - color: Style.current.separator - } + maxNumberOfRecipients: + recipientsCountInstantiator.maximumRecipientsCount - StatusItemSelector { - id: addressess + membersModel: SortFilterProxyModel { + sourceModel: membersModel - Layout.fillWidth: true - icon: Style.svg("member") - title: qsTr("To") - placeholderText: qsTr("Example: 12 addresses and 3 members") - tagLeftPadding: 2 - asset.height: 28 - asset.width: asset.height + filters: ExpressionFilter { + id: selectedKeysFilter - model: ListModel {} + property var keys: [] - addButton.onClicked: { - if(addressInput.text.length > 0) - model.append({text: addressInput.text}) + expression: keys.indexOf(model.pubKey) !== -1 + } } - onItemClicked: addressess.model.remove(index) + onRemoveMemberRequested: { + const pubKey = ModelUtils.get(membersModel, index, "pubKey") + const keyIndex = selectedKeysFilter.keys.indexOf(pubKey) + + selectedKeysFilter.keys.splice(keyIndex, 1) + selectedKeysFilter.keys = selectedKeysFilter.keys + } + + onAddAddressesRequested: (addresses_) => { + addresses.addAddressesFromString(addresses_) + airdropRecipientsSelector.clearAddressesInput() + airdropRecipientsSelector.positionAddressesListAtEnd() + } + + onRemoveAddressRequested: addresses.remove(index) + + ListModel { + id: addresses + + function addAddressesFromString(addresses) { + const words = addresses.trim().split(/[\s+,]/) + const existing = new Set() + + for (let i = 0; i < count; i++) + existing.add(get(i).address) + + words.forEach(word => { + if (word === "" || existing.has(word)) + return + + const valid = Utils.isValidAddress(word) + append({ valid, address: word }) + }) + } + } + + function openPopup(popup) { + popup.parent = addButton + popup.x = addButton.width + d.dropdownHorizontalOffset + popup.y = 0 + + popup.open() + } + + addButton.onClicked: openPopup(recipientTypeSelectionDropdown) + + RecipientTypeSelectionDropdown { + id: recipientTypeSelectionDropdown + + onEthAddressesSelected: { + airdropRecipientsSelector.showAddressesInputWhenEmpty = true + airdropRecipientsSelector.forceInputFocus() + recipientTypeSelectionDropdown.close() + } + + onCommunityMembersSelected: { + recipientTypeSelectionDropdown.close() + membersDropdown.selectedKeys = selectedKeysFilter.keys + airdropRecipientsSelector.openPopup(membersDropdown) + } + } + + MembersDropdown { + id: membersDropdown + + model: SortFilterProxyModel { + sourceModel: membersModel + + 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: { + close() + airdropRecipientsSelector.openPopup( + recipientTypeSelectionDropdown) + } + + onAddButtonClicked: { + selectedKeysFilter.keys = selectedKeys + close() + } + } } StatusButton { @@ -253,13 +361,14 @@ StatusScrollView { onClicked: { const airdropTokens = ModelUtils.modelToArray( root.selectedHoldingsModel, - ["key", "type", "amount"]) + ["key", "amount"]) - const addresses = ModelUtils.modelToArray( - addressess.model, - ["text"]) + const addresses_ = ModelUtils.modelToArray( + addresses, ["address"]).map(e => e.address) - root.airdropClicked(airdropTokens, addresses) + const pubKeys = selectedKeysFilter.keys + + root.airdropClicked(airdropTokens, addresses_, pubKeys) } } } diff --git a/ui/imports/shared/stores/CommunityTokensStore.qml b/ui/imports/shared/stores/CommunityTokensStore.qml index b11c1393e4..394dc187ea 100644 --- a/ui/imports/shared/stores/CommunityTokensStore.qml +++ b/ui/imports/shared/stores/CommunityTokensStore.qml @@ -54,10 +54,6 @@ QtObject { // Airdrop tokens: function airdrop(communityId, airdropTokens, addresses) { - const addrArray = [] - for(var i = 0; i < addresses.length; i++) { - addrArray.push(addresses[i]["text"]) - } - communityTokensModuleInst.airdropCollectibles(communityId, JSON.stringify(airdropTokens), JSON.stringify(addrArray)) + communityTokensModuleInst.airdropCollectibles(communityId, JSON.stringify(airdropTokens), JSON.stringify(addresses)) } }