diff --git a/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml b/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml index eedbd157a0..0a6f85e9f0 100644 --- a/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml +++ b/ui/app/AppLayouts/Chat/controls/community/HoldingsDropdown.qml @@ -14,6 +14,7 @@ StatusDropdown { property var assetsModel property var collectiblesModel + property bool isENSTab: true property var usedTokens: [] property var usedEnsNames: [] @@ -74,6 +75,8 @@ StatusDropdown { readonly property var holdingTypes: [ HoldingTypes.Type.Asset, HoldingTypes.Type.Collectible, HoldingTypes.Type.Ens ] + readonly property var tabsModel: [qsTr("Assets"), qsTr("Collectibles"), qsTr("ENS")] + readonly property var tabsModelNoEns: [qsTr("Assets"), qsTr("Collectibles")] readonly property bool assetsReady: root.assetAmount > 0 && root.assetKey readonly property bool collectiblesReady: root.collectibleAmount > 0 && root.collectibleKey readonly property bool ensReady: d.ensDomainNameValid @@ -179,13 +182,15 @@ StatusDropdown { ] onCurrentIndexChanged: { - d.currentHoldingType = d.holdingTypes[currentIndex] - d.setInitialFlow() + if(currentIndex >= 0) { + d.currentHoldingType = d.holdingTypes[currentIndex] + d.setInitialFlow() + } } Repeater { id: tabLabelsRepeater - model: [qsTr("Assets"), qsTr("Collectibles"), qsTr("ENS")] + model: root.isENSTab ? d.tabsModel : d.tabsModelNoEns StatusSwitchTabButton { text: modelData @@ -204,14 +209,14 @@ StatusDropdown { State { name: HoldingsDropdown.FlowType.Selected PropertyChanges {target: loader; sourceComponent: (d.currentHoldingType === HoldingTypes.Type.Asset) ? assetLayout : - ((d.currentHoldingType === HoldingTypes.Type.Collectible) ? collectibleLayout : ensLayout) } + ((d.currentHoldingType === HoldingTypes.Type.Collectible) ? collectibleLayout : ensLayout) } PropertyChanges {target: root; height: undefined} // use implicit height }, State { name: HoldingsDropdown.FlowType.List_Deep1 PropertyChanges {target: loader; sourceComponent: listLayout} PropertyChanges {target: root; height: d.extendedContentHeight} - PropertyChanges {target: d; extendedDeepNavigation: false} + PropertyChanges {target: d; extendedDeepNavigation: false} }, State { name: HoldingsDropdown.FlowType.List_Deep2 @@ -352,8 +357,8 @@ StatusDropdown { onRemoveClicked: root.removeClicked() Component.onCompleted: { - if (d.collectibleAmountText.length === 0 && root.collectibleAmount) - collectiblePanel.setAmount(root.collectibleAmount) + if (d.collectibleAmountText.length === 0 && root.collectibleAmount) + collectiblePanel.setAmount(root.collectibleAmount) } Connections { diff --git a/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml b/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml new file mode 100644 index 0000000000..433144b67a --- /dev/null +++ b/ui/app/AppLayouts/Chat/panels/communities/CommunityAirdropsSettingsPanel.qml @@ -0,0 +1,103 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Controls 0.1 +import StatusQ.Core.Utils 0.1 + +import AppLayouts.Chat.layouts 1.0 +import AppLayouts.Chat.views.communities 1.0 + +import utils 1.0 + +SettingsPageLayout { + id: root + + // Token models: + required property var assetsModel + required property var collectiblesModel + + property int viewWidth: 560 // by design + + signal airdropClicked(var airdropTokens, string address) + + // TODO: Update with stackmanager when #8736 is integrated + function navigateBack() { + stackManager.pop(StackView.Immediate) + } + + QtObject { + id: d + + readonly property string welcomeViewState: "WELCOME" + readonly property string newAirdropViewState: "NEW_AIRDROP" + + readonly property string welcomePageTitle: qsTr("Airdrops") + readonly property string newAirdropViewPageTitle: qsTr("New airdrop") + } + + content: StackView { + anchors.fill: parent + initialItem: welcomeView + + Component.onCompleted: stackManager.pushInitialState(d.welcomeViewState) + } + + state: stackManager.currentState + states: [ + State { + name: d.welcomeViewState + PropertyChanges {target: root; title: d.welcomePageTitle} + PropertyChanges {target: root; previousPageName: ""} + PropertyChanges {target: root; headerButtonVisible: true} + PropertyChanges {target: root; headerButtonText: qsTr("New Airdrop")} + PropertyChanges {target: root; headerWidth: root.viewWidth} + }, + State { + name: d.newAirdropViewState + PropertyChanges {target: root; title: d.newAirdropViewPageTitle} + PropertyChanges {target: root; previousPageName: d.welcomePageTitle} + PropertyChanges {target: root; headerButtonVisible: false} + PropertyChanges {target: root; headerWidth: 0} + } + ] + + onHeaderButtonClicked: stackManager.push(d.newAirdropViewState, newAirdropView, null, StackView.Immediate) + + StackViewStates { + id: stackManager + + stackView: root.contentItem + } + + // Mint tokens possible view contents: + Component { + id: welcomeView + + CommunityWelcomeSettingsView { + viewWidth: root.viewWidth + image: Style.png("community/airdrops8_1") + title: qsTr("Airdrop community tokens") + subtitle: qsTr("You can mint custom tokens and collectibles for your community") + checkersModel: [ + qsTr("Reward individual members with custom tokens for their contribution"), + qsTr("Incentivise joining, retention, moderation and desired behaviour"), + qsTr("Require holding a token or NFT to obtain exclusive membership rights") + ] + } + } + + Component { + id: newAirdropView + + CommunityNewAirdropView { + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + + onAirdropClicked: { + root.airdropClicked(airdropTokens, address) + stackManager.clear(d.welcomeViewState, StackView.Immediate) + } + } + } +} diff --git a/ui/app/AppLayouts/Chat/panels/communities/qmldir b/ui/app/AppLayouts/Chat/panels/communities/qmldir index cd1c9cd2a5..33298c261b 100644 --- a/ui/app/AppLayouts/Chat/panels/communities/qmldir +++ b/ui/app/AppLayouts/Chat/panels/communities/qmldir @@ -1,3 +1,4 @@ +CommunityAirdropsSettingsPanel 1.0 CommunityAirdropsSettingsPanel.qml CommunityColumnHeaderPanel 1.0 CommunityColumnHeaderPanel.qml CommunityMintTokensSettingsPanel 1.0 CommunityMintTokensSettingsPanel.qml CommunityPermissionsSettingsPanel 1.0 CommunityPermissionsSettingsPanel.qml diff --git a/ui/app/AppLayouts/Chat/stores/CommunityTokensStore.qml b/ui/app/AppLayouts/Chat/stores/CommunityTokensStore.qml index 0bf0fa0e3a..10a29d2303 100644 --- a/ui/app/AppLayouts/Chat/stores/CommunityTokensStore.qml +++ b/ui/app/AppLayouts/Chat/stores/CommunityTokensStore.qml @@ -55,4 +55,9 @@ QtObject { } ]) } + + // Airdrop tokens: + function airdrop(airdropTokens, address) { + console.warn("TODO: Airdrop backend call!") + } } diff --git a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml index b6b92c8386..f90b191a09 100644 --- a/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml +++ b/ui/app/AppLayouts/Chat/views/CommunitySettingsView.qml @@ -36,9 +36,9 @@ StatusSectionLayout { property var settingsMenuModel: [{name: qsTr("Overview"), icon: "show", enabled: true}, {name: qsTr("Members"), icon: "group-chat", enabled: true}, {name: qsTr("Permissions"), icon: "objects", enabled: root.rootStore.communityPermissionsEnabled}, - {name: qsTr("Mint Tokens"), icon: "token", enabled: root.rootStore.communityTokensEnabled}] + {name: qsTr("Mint Tokens"), icon: "token", enabled: root.rootStore.communityTokensEnabled}, + {name: qsTr("Airdrops"), icon: "airdrop", enabled: root.rootStore.communityTokensEnabled}] // TODO: Next community settings options: - // {name: qsTr("Airdrops"), icon: "airdrop"}, // {name: qsTr("Token sales"), icon: "token-sale"}, // {name: qsTr("Subscriptions"), icon: "subscription"}, property var rootStore @@ -310,6 +310,17 @@ StatusSectionLayout { } } + CommunityAirdropsSettingsPanel { + readonly property CommunityTokensStore communityTokensStore: + rootStore.communityTokensStore + + assetsModel: rootStore.assetsModel + collectiblesModel: rootStore.collectiblesModel + + onPreviousPageNameChanged: root.backButtonName = previousPageName + onAirdropClicked: communityTokensStore.airdrop(airdropTokens, chainId, address) + } + onCurrentIndexChanged: root.backButtonName = centerPanelContentLoader.item.children[d.currentIndex].previousPageName } } diff --git a/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml b/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml new file mode 100644 index 0000000000..f4ba6a3f6d --- /dev/null +++ b/ui/app/AppLayouts/Chat/views/communities/CommunityNewAirdropView.qml @@ -0,0 +1,246 @@ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 + +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 + +import utils 1.0 +import shared.panels 1.0 + +import AppLayouts.Chat.helpers 1.0 +import AppLayouts.Chat.panels.communities 1.0 +import AppLayouts.Chat.controls.community 1.0 + +// TEMPORAL - BASIC IMPLEMENTATION +StatusScrollView { + id: root + + // Token models: + required property var assetsModel + required property var collectiblesModel + + property int viewWidth: 560 // by design + + // roles: type, key, name, amount, imageSource + property var selectedHoldingsModel: ListModel {} + + readonly property bool isFullyFilled: selectedHoldingsModel.count > 0 && + addressess.itemsModel.count > 0 + + signal airdropClicked(var airdropTokens, string address) + + QtObject { + id: d + + readonly property int maxAirdropTokens: 5 + readonly property int dropdownHorizontalOffset: 4 + readonly property int dropdownVerticalOffset: 1 + } + + contentWidth: mainLayout.width + contentHeight: mainLayout.height + + ColumnLayout { + id: mainLayout + width: root.viewWidth + spacing: 0 + + StatusItemSelector { + id: tokensSelector + + property int editedIndex: -1 + + Layout.fillWidth: true + icon: Style.svg("token") + title: qsTr("What") + defaultItemText: qsTr("Example: 1 SOCK") + tagLeftPadding: 2 + asset.height: 28 + asset.width: asset.height + addButton.visible: itemsModel.count < d.maxAirdropTokens + + itemsModel: HoldingsSelectionModel { + sourceModel: root.selectedHoldingsModel + + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + } + + // TODO: All this code is repeated inside `CommunityNewPermissionView`. Check how to reuse it. + HoldingsDropdown { + id: dropdown + + assetsModel: root.assetsModel + collectiblesModel: root.collectiblesModel + isENSTab: false + + function addItem(type, item, amount) { + const key = item.key + + root.selectedHoldingsModel.append( + { type, key, amount }) + } + + function getHoldingIndex(key) { + return ModelUtils.indexOf(root.selectedHoldingsModel, "key", key) + } + + function prepareUpdateIndex(key) { + const itemIndex = tokensSelector.editedIndex + const existingIndex = getHoldingIndex(key) + + if (itemIndex !== -1 && existingIndex !== -1 && itemIndex !== existingIndex) { + const previousKey = root.selectedHoldingsModel.get(itemIndex).key + root.selectedHoldingsModel.remove(existingIndex) + return getHoldingIndex(previousKey) + } + + if (itemIndex === -1) { + return existingIndex + } + + return itemIndex + } + + onAddAsset: { + const modelItem = CommunityPermissionsHelpers.getTokenByKey( + root.assetsModel, key) + addItem(HoldingTypes.Type.Asset, modelItem, amount) + dropdown.close() + } + + onAddCollectible: { + const modelItem = CommunityPermissionsHelpers.getTokenByKey( + root.collectiblesModel, key) + addItem(HoldingTypes.Type.Collectible, modelItem, amount) + dropdown.close() + } + + onUpdateAsset: { + const itemIndex = prepareUpdateIndex(key) + const modelItem = CommunityPermissionsHelpers.getTokenByKey(root.assetsModel, key) + + root.selectedHoldingsModel.set( + itemIndex, { type: HoldingTypes.Type.Asset, key, amount }) + dropdown.close() + } + + onUpdateCollectible: { + const itemIndex = prepareUpdateIndex(key) + const modelItem = CommunityPermissionsHelpers.getTokenByKey( + root.collectiblesModel, key) + + root.selectedHoldingsModel.set( + itemIndex, + { type: HoldingTypes.Type.Collectible, key, amount }) + dropdown.close() + } + + onRemoveClicked: { + root.selectedHoldingsModel.remove(tokensSelector.editedIndex) + dropdown.close() + } + } + + addButton.onClicked: { + dropdown.parent = tokensSelector.addButton + dropdown.x = tokensSelector.addButton.width + d.dropdownHorizontalOffset + dropdown.y = 0 + dropdown.open() + + editedIndex = -1 + } + + onItemClicked: { + if (mouse.button !== Qt.LeftButton) + return + + dropdown.parent = item + dropdown.x = mouse.x + d.dropdownHorizontalOffset + dropdown.y = d.dropdownVerticalOffset + + const modelItem = tokensSelector.itemsModel.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) + dropdown.openUpdateFlow() + + editedIndex = index + } + } + + Rectangle { + Layout.leftMargin: 16 + Layout.preferredWidth: 2 + Layout.preferredHeight: 24 + color: Style.current.separator + } + + // TEMPORAL + StatusInput { + id: addressInput + + Layout.fillWidth: true + placeholderText: qsTr("Example: 0x7F47C2e18a4BBf5487E6fb082eC2D9Ab0E6d7999") + } + + Rectangle { + Layout.leftMargin: 16 + Layout.preferredWidth: 2 + Layout.preferredHeight: 24 + color: Style.current.separator + } + + StatusItemSelector { + id: addressess + + Layout.fillWidth: true + icon: Style.svg("member") + title: qsTr("To") + defaultItemText: qsTr("Example: 12 addresses and 3 members") + tagLeftPadding: 2 + asset.height: 28 + asset.width: asset.height + + addButton.onClicked: { + if(addressInput.text.length > 0) + itemsModel.append({text: addressInput.text}) + } + + onItemClicked: addressess.itemsModel.remove(index) + } + + StatusButton { + Layout.preferredHeight: 44 + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.topMargin: Style.current.bigPadding + text: qsTr("Create airdrop") + enabled: root.isFullyFilled + + onClicked: { + const airdropTokens = ModelUtils.modelToArray( + root.selectedHoldingsModel, + ["key", "type", "amount"]) + + root.airdropClicked(airdropTokens, addressess.itemsModel) + } + } + } +} diff --git a/ui/app/AppLayouts/Chat/views/communities/qmldir b/ui/app/AppLayouts/Chat/views/communities/qmldir index d77ecd52d8..3a1000ba77 100644 --- a/ui/app/AppLayouts/Chat/views/communities/qmldir +++ b/ui/app/AppLayouts/Chat/views/communities/qmldir @@ -1,5 +1,6 @@ ChannelsSelectionModel 1.0 ChannelsSelectionModel.qml CommunityCollectibleView 1.0 CommunityCollectibleView.qml +CommunityNewAirdropView 1.0 CommunityNewAirdropView.qml CommunityMintedTokensView 1.0 CommunityMintedTokensView.qml CommunityNewCollectibleView 1.0 CommunityNewCollectibleView.qml CommunityNewPermissionView 1.0 CommunityNewPermissionView.qml diff --git a/ui/imports/assets/icons/member.svg b/ui/imports/assets/icons/member.svg new file mode 100644 index 0000000000..c418d41ff8 --- /dev/null +++ b/ui/imports/assets/icons/member.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/imports/assets/icons/token.svg b/ui/imports/assets/icons/token.svg new file mode 100644 index 0000000000..4f68221c17 --- /dev/null +++ b/ui/imports/assets/icons/token.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/imports/assets/png/community/airdrops8_1.png b/ui/imports/assets/png/community/airdrops8_1.png new file mode 100644 index 0000000000..baa38a5e01 Binary files /dev/null and b/ui/imports/assets/png/community/airdrops8_1.png differ