2023-04-25 21:24:04 +00:00
|
|
|
import QtQuick 2.15
|
|
|
|
import QtQuick.Layouts 1.15
|
2023-03-13 16:32:14 +00:00
|
|
|
|
|
|
|
import StatusQ.Core 0.1
|
|
|
|
import StatusQ.Core.Theme 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
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
import SortFilterProxyModel 0.2
|
|
|
|
|
|
|
|
|
2023-03-13 16:32:14 +00:00
|
|
|
StatusScrollView {
|
|
|
|
id: root
|
|
|
|
|
|
|
|
// Token models:
|
|
|
|
required property var assetsModel
|
|
|
|
required property var collectiblesModel
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
// Community members model:
|
|
|
|
required property var membersModel
|
|
|
|
|
2023-03-13 16:32:14 +00:00
|
|
|
property int viewWidth: 560 // by design
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
readonly property var selectedHoldingsModel: ListModel {}
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
readonly property bool isFullyFilled: tokensSelector.count > 0 &&
|
|
|
|
airdropRecipientsSelector.count > 0 &&
|
|
|
|
airdropRecipientsSelector.valid
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
signal airdropClicked(var airdropTokens, var addresses, var membersPubKeys)
|
2023-03-24 12:41:46 +00:00
|
|
|
signal navigateToMintTokenSettings
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-06-01 10:38:56 +00:00
|
|
|
function selectToken(key, amount, type) {
|
|
|
|
var tokenModel = null
|
|
|
|
if(type === Constants.TokenType.ERC20)
|
|
|
|
tokenModel = root.assetsModel
|
|
|
|
else if (type === Constants.TokenType.ERC721)
|
|
|
|
tokenModel = root.collectiblesModel
|
|
|
|
|
2023-04-17 12:11:31 +00:00
|
|
|
const modelItem = CommunityPermissionsHelpers.getTokenByKey(
|
2023-06-01 10:38:56 +00:00
|
|
|
tokenModel, key)
|
2023-04-25 21:24:04 +00:00
|
|
|
|
2023-06-01 10:38:56 +00:00
|
|
|
const entry = d.prepareEntry(key, amount, type)
|
2023-04-25 21:24:04 +00:00
|
|
|
entry.valid = true
|
|
|
|
selectedHoldingsModel.append(entry)
|
2023-04-17 12:11:31 +00:00
|
|
|
}
|
|
|
|
|
2023-06-05 13:49:36 +00:00
|
|
|
function addAddresses(_addresses) {
|
|
|
|
addresses.addAddresses(_addresses)
|
|
|
|
}
|
|
|
|
|
2023-03-13 16:32:14 +00:00
|
|
|
QtObject {
|
|
|
|
id: d
|
|
|
|
|
|
|
|
readonly property int maxAirdropTokens: 5
|
|
|
|
readonly property int dropdownHorizontalOffset: 4
|
|
|
|
readonly property int dropdownVerticalOffset: 1
|
2023-04-17 12:11:31 +00:00
|
|
|
|
2023-06-01 10:38:56 +00:00
|
|
|
function prepareEntry(key, amount, type) {
|
|
|
|
var tokenModel = null
|
|
|
|
if(type === Constants.TokenType.ERC20)
|
|
|
|
tokenModel = root.assetsModel
|
|
|
|
else if (type === Constants.TokenType.ERC721)
|
|
|
|
tokenModel = root.collectiblesModel
|
|
|
|
|
|
|
|
const modelItem = CommunityPermissionsHelpers.getTokenByKey(tokenModel, key)
|
2023-04-25 21:24:04 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
key, amount,
|
|
|
|
tokenText: amount + " " + modelItem.name,
|
|
|
|
tokenImage: modelItem.iconSource,
|
|
|
|
networkText: modelItem.chainName,
|
|
|
|
networkImage: Style.svg(modelItem.chainIcon),
|
|
|
|
supply: modelItem.supply,
|
2023-05-25 10:00:38 +00:00
|
|
|
infiniteSupply: modelItem.infiniteSupply,
|
|
|
|
contractUniqueKey: modelItem.contractUniqueKey,
|
2023-04-25 21:24:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Instantiator {
|
|
|
|
id: recipientsCountInstantiator
|
|
|
|
|
|
|
|
model: selectedHoldingsModel
|
|
|
|
|
|
|
|
property bool infinity: true
|
|
|
|
property int maximumRecipientsCount
|
2023-04-17 12:11:31 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
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
|
2023-04-17 12:11:31 +00:00
|
|
|
}
|
2023-04-25 21:24:04 +00:00
|
|
|
|
|
|
|
delegate: QtObject {
|
|
|
|
readonly property int supply: model.supply
|
|
|
|
readonly property real amount: model.amount
|
|
|
|
readonly property bool infiniteSupply: model.infiniteSupply
|
|
|
|
|
2023-04-27 22:13:59 +00:00
|
|
|
readonly property bool valid:
|
|
|
|
infiniteSupply || amount * airdropRecipientsSelector.count <= supply
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
onSupplyChanged: recipientsCountInstantiator.findRecipientsCount()
|
|
|
|
onAmountChanged: recipientsCountInstantiator.findRecipientsCount()
|
|
|
|
onInfiniteSupplyChanged: recipientsCountInstantiator.findRecipientsCount()
|
2023-04-27 22:13:59 +00:00
|
|
|
|
|
|
|
onValidChanged: model.valid = valid
|
|
|
|
Component.onCompleted: model.valid = valid
|
2023-04-25 21:24:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
onCountChanged: findRecipientsCount()
|
2023-03-13 16:32:14 +00:00
|
|
|
}
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
SequenceColumnLayout {
|
2023-03-13 16:32:14 +00:00
|
|
|
id: mainLayout
|
|
|
|
width: root.viewWidth
|
|
|
|
spacing: 0
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
AirdropTokensSelector {
|
2023-03-13 16:32:14 +00:00
|
|
|
id: tokensSelector
|
|
|
|
|
|
|
|
property int editedIndex: -1
|
|
|
|
|
|
|
|
Layout.fillWidth: true
|
|
|
|
icon: Style.svg("token")
|
|
|
|
title: qsTr("What")
|
2023-03-13 10:02:41 +00:00
|
|
|
placeholderText: qsTr("Example: 1 SOCK")
|
|
|
|
addButton.visible: model.count < d.maxAirdropTokens
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
model: root.selectedHoldingsModel
|
2023-03-13 16:32:14 +00:00
|
|
|
|
|
|
|
HoldingsDropdown {
|
|
|
|
id: dropdown
|
|
|
|
|
|
|
|
assetsModel: root.assetsModel
|
|
|
|
collectiblesModel: root.collectiblesModel
|
|
|
|
isENSTab: false
|
2023-03-23 16:21:15 +00:00
|
|
|
isCollectiblesOnly: true
|
2023-03-23 18:26:38 +00:00
|
|
|
noDataText: qsTr("First you need to mint or import a collectible before you can perform an airdrop")
|
2023-03-13 16:32:14 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
onOpened: {
|
|
|
|
usedTokens = ModelUtils.modelToArray(
|
|
|
|
root.selectedHoldingsModel, ["key", "amount"])
|
2023-03-13 16:32:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
onAddCollectible: {
|
2023-06-01 10:38:56 +00:00
|
|
|
const entry = d.prepareEntry(key, amount, Constants.TokenType.ERC721)
|
2023-04-27 22:13:59 +00:00
|
|
|
entry.valid = true
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
selectedHoldingsModel.append(entry)
|
2023-03-13 16:32:14 +00:00
|
|
|
dropdown.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
onUpdateCollectible: {
|
|
|
|
const itemIndex = prepareUpdateIndex(key)
|
2023-04-25 21:24:04 +00:00
|
|
|
|
2023-06-01 10:38:56 +00:00
|
|
|
const entry = d.prepareEntry(key, amount, Constants.TokenType.ERC721)
|
2023-03-13 16:32:14 +00:00
|
|
|
const modelItem = CommunityPermissionsHelpers.getTokenByKey(
|
|
|
|
root.collectiblesModel, key)
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
root.selectedHoldingsModel.set(itemIndex, entry)
|
2023-03-13 16:32:14 +00:00
|
|
|
dropdown.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
onRemoveClicked: {
|
|
|
|
root.selectedHoldingsModel.remove(tokensSelector.editedIndex)
|
|
|
|
dropdown.close()
|
|
|
|
}
|
2023-03-24 12:41:46 +00:00
|
|
|
|
|
|
|
onNavigateToMintTokenSettings: {
|
|
|
|
root.navigateToMintTokenSettings()
|
|
|
|
close()
|
|
|
|
}
|
2023-03-13 16:32:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
const modelItem = selectedHoldingsModel.get(index)
|
|
|
|
dropdown.collectibleKey = modelItem.key
|
|
|
|
dropdown.collectibleAmount = modelItem.amount
|
|
|
|
dropdown.setActiveTab(HoldingTypes.Type.Collectible)
|
2023-03-13 16:32:14 +00:00
|
|
|
dropdown.openUpdateFlow()
|
|
|
|
|
|
|
|
editedIndex = index
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
SequenceColumnLayout.Separator {}
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
AirdropRecipientsSelector {
|
|
|
|
id: airdropRecipientsSelector
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
addressesModel: addresses
|
2023-03-13 10:16:08 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
infiniteMaxNumberOfRecipients:
|
|
|
|
recipientsCountInstantiator.infinity
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
maxNumberOfRecipients:
|
|
|
|
recipientsCountInstantiator.maximumRecipientsCount
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
membersModel: SortFilterProxyModel {
|
|
|
|
sourceModel: membersModel
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
filters: ExpressionFilter {
|
|
|
|
id: selectedKeysFilter
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-05-11 10:44:47 +00:00
|
|
|
property var keys: new Set()
|
2023-03-13 10:02:41 +00:00
|
|
|
|
2023-05-11 10:44:47 +00:00
|
|
|
expression: keys.has(model.pubKey)
|
2023-04-25 21:24:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onRemoveMemberRequested: {
|
|
|
|
const pubKey = ModelUtils.get(membersModel, index, "pubKey")
|
|
|
|
|
2023-05-11 10:44:47 +00:00
|
|
|
selectedKeysFilter.keys.delete(pubKey)
|
|
|
|
selectedKeysFilter.keys = new Set([...selectedKeysFilter.keys])
|
2023-04-25 21:24:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
onAddAddressesRequested: (addresses_) => {
|
|
|
|
addresses.addAddressesFromString(addresses_)
|
|
|
|
airdropRecipientsSelector.clearAddressesInput()
|
|
|
|
airdropRecipientsSelector.positionAddressesListAtEnd()
|
|
|
|
}
|
|
|
|
|
|
|
|
onRemoveAddressRequested: addresses.remove(index)
|
|
|
|
|
|
|
|
ListModel {
|
|
|
|
id: addresses
|
|
|
|
|
2023-06-05 13:49:36 +00:00
|
|
|
function addAddresses(_addresses) {
|
2023-04-25 21:24:04 +00:00
|
|
|
const existing = new Set()
|
|
|
|
|
|
|
|
for (let i = 0; i < count; i++)
|
|
|
|
existing.add(get(i).address)
|
|
|
|
|
2023-06-05 13:49:36 +00:00
|
|
|
_addresses.forEach(address => {
|
|
|
|
if (existing.has(address))
|
2023-04-25 21:24:04 +00:00
|
|
|
return
|
|
|
|
|
2023-06-05 13:49:36 +00:00
|
|
|
const valid = Utils.isValidAddress(address)
|
|
|
|
append({ valid, address })
|
2023-04-25 21:24:04 +00:00
|
|
|
})
|
|
|
|
}
|
2023-06-05 13:49:36 +00:00
|
|
|
|
|
|
|
function addAddressesFromString(addressesString) {
|
|
|
|
const words = addressesString.trim().split(/[\s+,]/)
|
|
|
|
const wordsNonEmpty = words.filter(word => !!word)
|
|
|
|
|
|
|
|
addAddresses(wordsNonEmpty)
|
|
|
|
}
|
2023-04-25 21:24:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function openPopup(popup) {
|
|
|
|
popup.parent = addButton
|
|
|
|
popup.x = addButton.width + d.dropdownHorizontalOffset
|
|
|
|
popup.y = 0
|
|
|
|
|
|
|
|
popup.open()
|
2023-03-13 16:32:14 +00:00
|
|
|
}
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
addButton.onClicked: openPopup(recipientTypeSelectionDropdown)
|
|
|
|
|
|
|
|
RecipientTypeSelectionDropdown {
|
|
|
|
id: recipientTypeSelectionDropdown
|
|
|
|
|
|
|
|
onEthAddressesSelected: {
|
|
|
|
airdropRecipientsSelector.showAddressesInputWhenEmpty = true
|
|
|
|
airdropRecipientsSelector.forceInputFocus()
|
|
|
|
recipientTypeSelectionDropdown.close()
|
|
|
|
}
|
|
|
|
|
|
|
|
onCommunityMembersSelected: {
|
|
|
|
recipientTypeSelectionDropdown.close()
|
|
|
|
membersDropdown.selectedKeys = selectedKeysFilter.keys
|
2023-04-28 09:40:58 +00:00
|
|
|
|
2023-05-11 10:44:47 +00:00
|
|
|
const hasSelection = selectedKeysFilter.keys.size !== 0
|
2023-04-28 09:40:58 +00:00
|
|
|
|
|
|
|
membersDropdown.mode = hasSelection
|
|
|
|
? MembersDropdown.Mode.Update
|
|
|
|
: MembersDropdown.Mode.Add
|
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
airdropRecipientsSelector.openPopup(membersDropdown)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
MembersDropdown {
|
|
|
|
id: membersDropdown
|
|
|
|
|
2023-04-28 09:40:58 +00:00
|
|
|
forceButtonDisabled:
|
|
|
|
mode === MembersDropdown.Mode.Update &&
|
2023-05-11 10:44:47 +00:00
|
|
|
[...selectedKeys].sort().join() === [...selectedKeysFilter.keys].sort().join()
|
2023-04-28 09:40:58 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
model: SortFilterProxyModel {
|
|
|
|
sourceModel: membersModel
|
|
|
|
|
|
|
|
filters: [
|
|
|
|
ExpressionFilter {
|
|
|
|
enabled: membersDropdown.searchText !== ""
|
|
|
|
|
|
|
|
function matchesAlias(name, filter) {
|
|
|
|
return name.split(" ").some(p => p.startsWith(filter))
|
|
|
|
}
|
|
|
|
|
|
|
|
expression: {
|
|
|
|
membersDropdown.searchText
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
2023-03-13 16:32:14 +00:00
|
|
|
}
|
|
|
|
|
2023-04-27 22:13:59 +00:00
|
|
|
WarningPanel {
|
|
|
|
Layout.fillWidth: true
|
|
|
|
Layout.topMargin: Style.current.padding
|
|
|
|
|
|
|
|
text: qsTr("Not enough tokens to send to all recipients. Reduce the number of recipients or change the number of tokens sent to each recipient.")
|
|
|
|
|
|
|
|
visible: !recipientsCountInstantiator.infinity &&
|
|
|
|
recipientsCountInstantiator.maximumRecipientsCount < airdropRecipientsSelector.count
|
|
|
|
}
|
|
|
|
|
2023-03-13 16:32:14 +00:00
|
|
|
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,
|
2023-05-25 10:00:38 +00:00
|
|
|
["contractUniqueKey", "amount"])
|
2023-04-25 21:24:04 +00:00
|
|
|
|
|
|
|
const addresses_ = ModelUtils.modelToArray(
|
|
|
|
addresses, ["address"]).map(e => e.address)
|
2023-03-13 16:32:14 +00:00
|
|
|
|
2023-05-11 10:44:47 +00:00
|
|
|
const pubKeys = [...selectedKeysFilter.keys]
|
2023-03-13 10:16:08 +00:00
|
|
|
|
2023-04-25 21:24:04 +00:00
|
|
|
root.airdropClicked(airdropTokens, addresses_, pubKeys)
|
2023-03-13 16:32:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|