feat(Airdrops): Component for selecting recipient addresses and members

Closes: #9799
This commit is contained in:
Michał Cieślak 2023-03-31 14:07:33 +02:00 committed by Michał
parent 0ebc5e4194
commit 6768f62451
12 changed files with 868 additions and 10 deletions

View File

@ -165,6 +165,18 @@ ListModel {
title: "StatusGroupBox"
section: "Components"
}
ListElement {
title: "AddressesInputList"
section: "Components"
}
ListElement {
title: "AddressesSelectorPanel"
section: "Components"
}
ListElement {
title: "AirdropRecipientsSelector"
section: "Components"
}
ListElement {
title: "AirdropTokensSelector"
section: "Components"

View File

@ -3,6 +3,18 @@
"https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1159%3A114479",
"https://www.figma.com/file/idUoxN7OIW2Jpp3PMJ1Rl8/%E2%9A%99%EF%B8%8F-Settings-%7C-Desktop?node-id=1684%3A127762"
],
"AirdropRecipientsSelector": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-494998",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-495258",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22647-497754",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=28045-533663",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=28045-533912",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-495493",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-495928",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-496145",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22642-496092",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22647-498080"
],
"AirdropTokensSelector": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22602-495563",
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22628-494998",

View File

@ -0,0 +1,71 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import AppLayouts.Chat.controls.community 1.0
import Storybook 1.0
import Models 1.0
SplitView {
orientation: Qt.Vertical
Logs { id: logs }
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
Rectangle {
anchors.fill: parent
color: "lightgray"
}
AddressesInputList {
width: 500
anchors.centerIn: parent
enabled: isEnabledCheckBox.checked
model: AddressesModel {
id: addressesModel
}
onAddAddressesRequested: {
addressesModel.addAddressesFromString(addresses)
clearInput()
positionListAtEnd()
}
onRemoveAddressRequested: addressesModel.remove(index)
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 160
logsView.logText: logs.logText
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
CheckBox {
id: isEnabledCheckBox
text: "Enabled"
checked: true
}
Button {
text: "Clear"
onClicked: addressesModel.clear()
}
}
}
}

View File

@ -0,0 +1,79 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import AppLayouts.Chat.controls.community 1.0
import Storybook 1.0
import Models 1.0
SplitView {
orientation: Qt.Vertical
Logs { id: logs }
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
Rectangle {
anchors.fill: parent
color: "lightgray"
}
Timer {
id: timer
interval: 1000
onTriggered: {
addressesModel.addAddressesFromString(
addressesSelectorPanel.text)
addressesSelectorPanel.clearInput()
addressesSelectorPanel.positionListAtEnd()
}
}
AddressesSelectorPanel {
id: addressesSelectorPanel
anchors.centerIn: parent
width: 500
model: AddressesModel {
id: addressesModel
}
Binding on loading { value: isLoadingCheckBox.checked }
Binding on loading { value: timer.running }
onAddAddressesRequested: timer.start()
onRemoveAddressRequested: addressesModel.remove(index)
}
}
LogsAndControlsPanel {
SplitView.minimumHeight: 100
SplitView.preferredHeight: 160
logsView.logText: logs.logText
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
CheckBox {
id: isLoadingCheckBox
text: "Is loading"
}
Button {
text: "Clear"
onClicked: addressesModel.clear()
}
}
}
}

View File

@ -0,0 +1,175 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import AppLayouts.Chat.controls.community 1.0
import Models 1.0
import Storybook 1.0
import utils 1.0
SplitView {
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 = {}
}
}
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
AddressesModel {
id: addresses
}
ListModel {
id: members
property int counter: 0
function addMember() {
const i = counter++
const key = `pub_key_${i}`
append({
alias: "",
colorId: "1",
displayName: `contact ${i}`,
ensName: "",
icon: "",
isContact: true,
localNickname: "",
onlineStatus: 1,
pubKey: key,
isVerified: true,
isUntrustworthy: false
})
}
Component.onCompleted: {
for (let i = 0; i < 4; i++)
addMember()
}
}
Loader {
id: loader
anchors.centerIn: parent
active: globalUtilsReady && mainModuleReady
sourceComponent: AirdropRecipientsSelector {
id: selector
addressesModel: addresses
loadingAddresses: timer.running
membersModel: members
showAddressesInputWhenEmpty:
showAddressesInputWhenEmptyCheckBox.checked
onAddAddressesRequested: timer.start()
onRemoveAddressRequested: addresses.remove(index)
onRemoveMemberRequested: members.remove(index)
Timer {
id: timer
interval: 1000
onTriggered: {
addresses.addAddressesFromString(
selector.addressesInputText)
selector.clearAddressesInput()
selector.positionAddressesListAtEnd()
}
}
}
}
}
LogsAndControlsPanel {
SplitView.minimumHeight: 100
SplitView.preferredHeight: 180
logsView.logText: logs.logText
ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
RowLayout {
Button {
text: "Clear addresses list"
onClicked: addresses.clear()
}
Button {
text: "Clear members list"
onClicked: members.clear()
}
CheckBox {
id: showAddressesInputWhenEmptyCheckBox
text: "Show addresses input when empty"
}
}
Button {
text: "Add member"
onClicked: {
members.addMember()
loader.item.positionMembersListAtEnd()
}
}
MenuSeparator {}
TextEdit {
readOnly: true
selectByMouse: true
text: "valid address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc4"
}
}
}
}

View File

@ -0,0 +1,30 @@
import QtQuick 2.15
import utils 1.0
ListModel {
ListElement {
address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
valid: true
}
ListElement {
address: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756ccx"
valid: false
}
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 })
})
}
}

View File

@ -1,13 +1,14 @@
singleton ModelsData 1.0 ModelsData.qml
singleton PermissionsModel 1.0 PermissionsModel.qml
singleton NetworksModel 1.0 NetworksModel.qml
singleton MintedCollectiblesModel 1.0 MintedCollectiblesModel.qml
IconModel 1.0 IconModel.qml
BannerModel 1.0 BannerModel.qml
UsersModel 1.0 UsersModel.qml
AssetsModel 1.0 AssetsModel.qml
CollectiblesModel 1.0 CollectiblesModel.qml
ChannelsModel 1.0 ChannelsModel.qml
AddressesModel 1.0 AddressesModel.qml
AssetsCollectiblesIconsModel 1.0 AssetsCollectiblesIconsModel.qml
AssetsModel 1.0 AssetsModel.qml
BannerModel 1.0 BannerModel.qml
ChannelsModel 1.0 ChannelsModel.qml
CollectiblesModel 1.0 CollectiblesModel.qml
IconModel 1.0 IconModel.qml
TokenHoldersModel 1.0 TokenHoldersModel.qml
UsersModel 1.0 UsersModel.qml
WalletAccountsModel 1.0 WalletAccountsModel.qml
singleton MintedCollectiblesModel 1.0 MintedCollectiblesModel.qml
singleton ModelsData 1.0 ModelsData.qml
singleton NetworksModel 1.0 NetworksModel.qml
singleton PermissionsModel 1.0 PermissionsModel.qml

View File

@ -0,0 +1,203 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
Control {
id: root
property alias model: listView.model
readonly property alias count: listView.count
property string text: listView.footerItem.text
property int maximumTextInputHeight: 156
property int maximumHeight: 405
signal addAddressesRequested(string addresses)
signal removeAddressRequested(int index)
function clearInput() {
listView.footerItem.edit.clear()
}
function positionListAtEnd() {
listView.positionViewAtEnd()
}
padding: 8
rightPadding: 13
clip: true
QtObject {
id: d
readonly property int delegateHeight: 32
readonly property int spacing: 8
readonly property int scrollBarWidth: 4
readonly property int scrollBarOffset: 5
}
background: Rectangle {
radius: Style.current.radius
color: Theme.palette.indirectColor1
}
contentItem: StatusListView {
id: listView
readonly property int maximumHeight:
root.maximumHeight - root.bottomPadding - root.topPadding
clip: false
verticalScrollBar {
implicitWidth: d.scrollBarWidth + ScrollBar.vertical.padding * 2
parent: listView.parent
anchors {
left: listView.right
top: listView.top
bottom: listView.bottom
leftMargin: -verticalScrollBar.leftPadding + d.scrollBarOffset
}
}
spacing: d.spacing
implicitHeight: Math.min(contentHeight, maximumHeight)
implicitWidth: root.availableWidth
delegate: Rectangle {
id: delegate
radius: height / 2
color: Theme.palette.directColor8
width: ListView.view.width
height: d.delegateHeight
states: State {
when: !model.valid
PropertyChanges {
target: delegate
color: Theme.palette.alphaColor(
Theme.palette.dangerColor1, 0.05)
}
PropertyChanges {
target: statusIcon
width: 21
height: 21
icon: "warning"
color: Theme.palette.dangerColor1
}
PropertyChanges {
target: addressText
color: Theme.palette.dangerColor1
}
}
StatusIcon {
id: statusIcon
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.left
anchors.horizontalCenterOffset: 18
width: 16
height: 16
icon: "checkbox"
color: Theme.palette.successColor1
}
StatusBaseText {
id: addressText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: deleteIcon.left
anchors.margins: 7
anchors.leftMargin: 34
color: Theme.palette.directColor1
font.pixelSize: 15
font.weight: Font.Medium
elide: Text.ElideMiddle
text: model.address
}
StatusIcon {
id: deleteIcon
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 10
width: 16
height: 16
icon: "delete"
color: Theme.palette.directColor1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
root.removeAddressRequested(model.index)
}
}
}
}
footer: StatusBaseInput {
id: input
showBackground: false
maximumLength: 2000
width: root.availableWidth
leftPadding: 0
rightPadding: 0
multiline: true
topPadding: bottomPadding + (listView.count ? d.spacing : 0)
height: edit.implicitHeight + topPadding + bottomPadding
placeholderText: qsTr("Example: 0x39cf...fbd2")
Keys.onPressed: {
if ((event.key !== Qt.Key_Return && event.key !== Qt.Key_Enter)
|| event.modifiers & Qt.ShiftModifier) {
event.accepted = false
return
}
event.accepted = true
if (input.text.length > 0)
root.addAddressesRequested(input.text)
}
onHeightChanged: Qt.callLater(() => listView.positionViewAtEnd())
verticalAlignment: Qt.AlignTop
placeholder.verticalAlignment: Qt.AlignTop
}
}
}

View File

@ -0,0 +1,103 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Components 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import SortFilterProxyModel 0.2
Control {
id: root
property alias model: addressesInputList.model
property alias text: addressesInputList.text
property bool loading: false
signal addAddressesRequested(string addresses)
signal removeAddressRequested(int index)
readonly property alias count: addressesInputList.count
readonly property alias validAddressesCount: validAddressesModel.count
readonly property int invalidAddressesCount: addressesInputList.count
- validAddressesCount
function clearInput() {
addressesInputList.clearInput()
}
function positionListAtEnd() {
addressesInputList.positionListAtEnd()
}
contentItem: Column {
spacing: 8
RowLayout {
width: root.availableWidth
spacing: 0
StatusBaseText {
color: Theme.palette.baseColor1
text: qsTr("ETH addresses")
font.pixelSize: Theme.tertiaryTextFontSize
elide: Text.ElideRight
}
Item { Layout.fillWidth: true }
StatusBaseText {
visible: !root.loading && root.validAddressesCount > 0
color: Theme.palette.baseColor1
text: qsTr("%n valid address(s)", "", root.validAddressesCount)
+ (root.invalidAddressesCount > 0 ? " / " : "")
font.pixelSize: Theme.tertiaryTextFontSize
}
StatusBaseText {
visible: !root.loading && root.invalidAddressesCount > 0
color: Theme.palette.dangerColor1
text: root.validAddressesCount > 0
? qsTr("%n invalid",
"invalid addresses, where \"addresses\" is implicit",
root.invalidAddressesCount)
: qsTr("%n invalid address(s)", "", root.invalidAddressesCount)
font.pixelSize: Theme.tertiaryTextFontSize
}
StatusLoadingIndicator {
visible: root.loading
Layout.preferredWidth: 10
Layout.preferredHeight: 10
Layout.rightMargin: 2
}
}
SortFilterProxyModel {
id: validAddressesModel
sourceModel: root.model ?? null
filters: ValueFilter {
roleName: "valid"
value: true
}
}
AddressesInputList {
id: addressesInputList
enabled: !root.loading
width: root.availableWidth
Component.onCompleted: {
addAddressesRequested.connect(root.addAddressesRequested)
removeAddressRequested.connect(root.removeAddressRequested)
}
}
}
}

View File

@ -0,0 +1,65 @@
import QtQuick 2.15
import StatusQ.Components 0.1
import utils 1.0
StatusFlowSelector {
id: root
property alias addressesModel: addressesSelectorPanel.model
property alias membersModel: membersSelectorPanel.model
property alias loadingAddresses: addressesSelectorPanel.loading
property alias addressesInputText: addressesSelectorPanel.text
property bool showAddressesInputWhenEmpty: false
signal addAddressesRequested(string addresses)
signal removeAddressRequested(int index)
signal removeMemberRequested(int index)
placeholderItem.visible: !addressesSelectorPanel.visible &&
!membersSelectorPanel.visible
title: qsTr("To")
icon: Style.svg("member")
flowSpacing: 12
placeholderText: qsTr("Example: 12 addresses and 3 members")
function clearAddressesInput() {
addressesSelectorPanel.clearInput()
}
function positionAddressesListAtEnd() {
addressesSelectorPanel.positionListAtEnd()
}
function positionMembersListAtEnd() {
membersSelectorPanel.positionListAtEnd()
}
AddressesSelectorPanel {
id: addressesSelectorPanel
visible: count > 0 || root.showAddressesInputWhenEmpty
width: root.availableWidth
Component.onCompleted: {
addAddressesRequested.connect(root.addAddressesRequested)
removeAddressRequested.connect(root.removeAddressRequested)
}
}
MembersSelectorPanel {
id: membersSelectorPanel
visible: count > 0
width: root.availableWidth
Component.onCompleted: removeMemberRequested.connect(
root.removeMemberRequested)
}
}

View File

@ -0,0 +1,103 @@
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 shared.controls.delegates 1.0
import utils 1.0
Control {
id: root
property alias model: listView.model
property int maximumListHeight: 188
readonly property alias count: listView.count
signal removeMemberRequested(int index)
function positionListAtEnd() {
listView.positionViewAtEnd()
}
QtObject {
id: d
readonly property int delegateHeight: 47
}
contentItem: Column {
spacing: 8
RowLayout {
width: root.availableWidth
spacing: 0
component Text: StatusBaseText {
color: Theme.palette.baseColor1
text: qsTr("Members")
font.pixelSize: Theme.tertiaryTextFontSize
elide: Text.ElideRight
}
Text {
text: qsTr("Members")
}
Item { Layout.fillWidth: true }
Text {
text: qsTr("%n member(s)", "", root.count)
}
}
Rectangle {
width: root.availableWidth
height: Math.min(root.maximumListHeight,
d.delegateHeight * root.count)
radius: Style.current.radius
color: Theme.palette.indirectColor1
StatusListView {
id: listView
anchors.fill: parent
delegate: ContactListItemDelegate {
width: ListView.view.width
height: d.delegateHeight
asset.width: 29
asset.height: 29
color: "transparent"
StatusIcon {
id: deleteIcon
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: 10
width: 16
height: 16
icon: "delete"
color: Theme.palette.directColor1
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.removeMemberRequested(model.index)
}
}
}
}
}
}
}

View File

@ -1,9 +1,13 @@
AddressesInputList 1.0 AddressesInputList.qml
AddressesSelectorPanel 1.0 AddressesSelectorPanel.qml
AirdropRecipientsSelector 1.0 AirdropRecipientsSelector.qml
AirdropTokensSelector 1.0 AirdropTokensSelector.qml
CommunityCategoryListItem 1.0 CommunityCategoryListItem.qml
CommunityListItem 1.0 CommunityListItem.qml
HoldingTypes 1.0 HoldingTypes.qml
HoldingsDropdown 1.0 HoldingsDropdown.qml
InDropdown 1.0 InDropdown.qml
MembersSelectorPanel 1.0 MembersSelectorPanel.qml
PermissionItem 1.0 PermissionItem.qml
PermissionsDropdown 1.0 PermissionsDropdown.qml
singleton PermissionTypes 1.0 PermissionTypes.qml