feat(CommunityMintTokens): added sortable token holders list component

This commit is contained in:
Michał Cieślak 2023-06-01 23:23:43 +02:00 committed by Michał
parent d479077e60
commit 9f73d874c2
9 changed files with 694 additions and 10 deletions

View File

@ -1,4 +1,4 @@
import QtQuick 2.14
import QtQuick 2.15
ListModel {
ListElement {
@ -125,6 +125,10 @@ ListModel {
title: "TokenHoldersPanel"
section: "Panels"
}
ListElement {
title: "SortableTokenHoldersPanel"
section: "Panels"
}
ListElement {
title: "ProfileSocialLinksPanel"
section: "Panels"
@ -309,6 +313,10 @@ ListModel {
title: "StatusChatListItem"
section: "Components"
}
ListElement {
title: "SortableTokenHoldersList"
section: "Components"
}
ListElement {
title: "BrowserSettings"
section: "Settings"

View File

@ -47,6 +47,10 @@ SplitView {
chainIcon: ModelsData.networks.ethereum
accountName: "helloworld"
tokenOwnersModel: TokenHoldersModel {
}
onMintCollectible: logs.logEvent("CommunityTokenView::onMintCollectible: \n"
+ "artworkSource: " + artworkSource + "\n"
+ "name: " + name + "\n"

View File

@ -0,0 +1,50 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import AppLayouts.Chat.panels.communities 1.0
import Storybook 1.0
import Models 1.0
SplitView {
id: root
Logs { id: logs }
orientation: Qt.Vertical
TokenHoldersModel {
id: tokenHoldersModel
}
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
SortableTokenHoldersList {
id: holdersList
anchors.fill: parent
anchors.margins: 50
model: TokenHoldersProxyModel {
sourceModel: tokenHoldersModel
sortBy: holdersList.sortBy
sortOrder: holdersList.sorting === SortableTokenHoldersList.Sorting.Descending
? Qt.DescendingOrder : Qt.AscendingOrder
}
onClicked: logs.logEvent("holdersList.clicked: " + index)
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 200
logsView.logText: logs.logText
}
}

View File

@ -0,0 +1,76 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import AppLayouts.Chat.panels.communities 1.0
import Storybook 1.0
import Models 1.0
SplitView {
id: root
Logs { id: logs }
orientation: Qt.Vertical
Item {
SplitView.fillWidth: true
SplitView.fillHeight: true
SortableTokenHoldersPanel {
id: holdersPanel
anchors.centerIn: parent
width: 568
tokenName: "Aniversary"
TokenHoldersModel {
id: tokenHoldersModel
}
ListModel {
id: emptyModel
}
model: emptyCheckBox.checked ? emptyModel : tokenHoldersModel
showRemotelyDestructMenuItem: remotelyDestructCheckBox.checked
onViewProfileRequested:
logs.logEvent("onViewProfileRequested: " + address)
onViewMessagesRequested:
logs.logEvent("onViewMessagesRequested: " + address)
onAirdropRequested:
logs.logEvent("onAirdropRequested: " + address)
onRemoteDestructRequested:
logs.logEvent("onRemoteDestructRequested: " + address)
onKickRequested:
logs.logEvent("onKickRequested: " + address)
onBanRequested:
logs.logEvent("onBanRequested: " + address)
}
}
LogsAndControlsPanel {
id: logsAndControlsPanel
SplitView.minimumHeight: 100
SplitView.preferredHeight: 200
logsView.logText: logs.logText
ColumnLayout {
CheckBox {
id: emptyCheckBox
text: "Empty"
}
CheckBox {
id: remotelyDestructCheckBox
checked: true
text: "Show \"Remotely Destruct\" menu item"
}
}
}
}

View File

@ -5,30 +5,49 @@ ListModel {
readonly property string image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAlklEQVR4nOzW0QmDQBAG4SSkl7SUQlJGCrElq9F3QdjjVhh/5nv3cFhY9vUIYQiNITSG0BhCExPynn1gWf9bx498P7/
nzPcxEzGExhBdJGYihtAYQlO+tUZvqrPbqeudo5iJGEJjCE15a3VtodH3q2ImYgiNITTlTdG1nUZ5a92VITQxITFiJmIIjSE0htAYQrMHAAD//+wwFVpz+yqXAAAAAElFTkSuQmCC"
readonly property var data: [
{
name: "carmen.eth",
walletAddress: "0xb794f5450ba39494ce839613fffba74279579261",
imageSource: image,
amount: 15
},
{
name: "chris.eth",
walletAddress: "0xb794f5ea0ba39494ce839613fffba74279579262",
imageSource: image,
amount: 5
amount: 5,
noOfMessages: 3123
},
{
name: "carmen.eth",
walletAddress: "0xb794f5450ba39494ce839613fffba74279579261",
imageSource: image,
amount: 15,
noOfMessages: 123
},
{
name: "emily.eth",
walletAddress: "0xb794f5ea0ba39494ce839613fffba74279579263",
imageSource: image,
amount: 2
amount: 2,
noOfMessages: 3
},
{
name: "",
walletAddress: "0xb794f5ea0ba39494ce839613fffba74279579268",
imageSource: "",
amount: 1
amount: 1,
noOfMessages: 0
},
{
name: "",
walletAddress: "0xc794f5ea0ba39494ce839613fffba74279579268",
imageSource: "",
amount: 11,
noOfMessages: 0
},
{
name: "",
walletAddress: "0xd794f5ea0ba39494ce839613fffba74279579268",
imageSource: "",
amount: 14,
noOfMessages: 0
}
]
Component.onCompleted: append(data)

View File

@ -0,0 +1,273 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Utils 0.1 as StatusQUtils
import utils 1.0
/*!
\qmltype SortableTokenHoldersList
\inherits StatusListView
\brief Shows list of users or addresses with corrensponding numbers of
messages and holding amounts.
Expected roles: name, walletAddress, imageSource, noOfMessages, amount
*/
StatusListView {
id: root
enum SortBy {
None, Username, NoOfMessages, Holding
}
enum Sorting {
Descending, Ascending
}
readonly property alias sortBy: d.sortBy
readonly property alias sorting: d.sorting
signal clicked(int index, var parent, var mouse)
currentIndex: -1
component ColumnHeader: StatusSortableColumnHeader {
id: columnHeader
leftPadding: 0
rightPadding: 4
Connections {
target: d
function onResetOtherHeaders(header) {
if (header !== columnHeader)
columnHeader.reset()
}
}
onClicked: {
d.resetOtherHeaders(this)
if (sorting === StatusSortableColumnHeader.Sorting.Ascending)
d.sorting = SortableTokenHoldersList.Sorting.Ascending
else if (sorting === StatusSortableColumnHeader.Sorting.Descending)
d.sorting = SortableTokenHoldersList.Sorting.Descending
}
}
component NumberCell: StatusBaseText {
horizontalAlignment: Qt.AlignRight
font.weight: Font.Medium
font.pixelSize: 13
color: Theme.palette.baseColor1
elide: Qt.ElideRight
}
QtObject {
id: d
property int sortBy: SortableTokenHoldersList.SortBy.None
property int sorting: SortableTokenHoldersList.Sorting.Descending
readonly property int red2Color: 4
signal resetOtherHeaders(var header)
}
header: ItemDelegate {
width: ListView.view.width
padding: 0
horizontalPadding: Style.current.padding
readonly property alias usernameHeaderWidth: usernameHeader.width
readonly property alias noOfMessagesHeaderWidth: noOfMessagesHeader.width
readonly property alias holdingHeaderWidth: holdingHeader.width
contentItem: RowLayout {
id: row
spacing: Style.current.padding
RowLayout {
id: usernameHeader
ColumnHeader {
text: qsTr("Username")
traversalOrder: [
StatusSortableColumnHeader.Sorting.NoSorting,
StatusSortableColumnHeader.Sorting.Ascending,
StatusSortableColumnHeader.Sorting.Descending
]
onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = SortableTokenHoldersList.SortBy.Username
else
d.sortBy = SortableTokenHoldersList.SortBy.None
}
}
Item {
Layout.fillWidth: true
}
}
ColumnHeader {
id: noOfMessagesHeader
text: qsTr("No. of messages")
onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = SortableTokenHoldersList.SortBy.NoOfMessages
else
d.sortBy = SortableTokenHoldersList.SortBy.None
}
}
RowLayout {
id: holdingHeader
Item {
Layout.preferredWidth: 25
}
ColumnHeader {
text: qsTr("Hodling")
onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = SortableTokenHoldersList.SortBy.Holding
else
d.sortBy = SortableTokenHoldersList.SortBy.None
}
}
}
}
}
delegate: ItemDelegate {
id: delegate
padding: 0
horizontalPadding: Style.current.padding
topPadding: showSeparator ? 10 : 0
readonly property string name: model.name
readonly property bool isFirstRowAddress: {
if (model.name !== "")
return false
const item = root.itemAtIndex(index - 1)
return item && item.name
}
readonly property bool showSeparator: isFirstRowAddress
&& root.sortBy === SortableTokenHoldersList.SortBy.Username
width: ListView.view.width
background: Item {
Rectangle {
anchors.fill: parent
anchors.topMargin: delegate.topPadding
radius: Style.current.radius
color: (delegate.hovered || delegate.ListView.isCurrentItem)
? Theme.palette.baseColor2 : "transparent"
}
Rectangle {
visible: delegate.showSeparator
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: delegate.topPadding / 2
height: 1
color: Theme.palette.baseColor2
}
}
contentItem: Item {
implicitWidth: delegateRow.implicitWidth
implicitHeight: delegateRow.implicitHeight
RowLayout {
id: delegateRow
spacing: Style.current.padding
StatusListItem {
id: listItem
readonly property bool unknownHolder: model.name === ""
readonly property string formattedTitle: unknownHolder
? "?" : model.name
readonly property string addressElided:
StatusQUtils.Utils.elideText(
model.walletAddress, 6, 3).replace("0x", "0×")
Layout.preferredWidth: root.headerItem.usernameHeaderWidth
color: "transparent"
leftPadding: 0
rightPadding: 0
sensor.enabled: false
title: unknownHolder ? addressElided : model.name
statusListItemIcon.name: "?"
subTitle: unknownHolder ? "" : addressElided
statusListItemSubTitle.font.pixelSize: Theme.asideTextFontSize
statusListItemSubTitle.lineHeightMode: Text.FixedHeight
statusListItemSubTitle.lineHeight: 14
asset.name: model.imageSource
asset.isImage: true
asset.isLetterIdenticon: unknownHolder
asset.color: Theme.palette.userCustomizationColors[d.red2Color]
}
NumberCell {
Layout.preferredWidth: root.headerItem.noOfMessagesHeaderWidth
text: model.name
? LocaleUtils.numberToLocaleString(model.noOfMessages)
: "-"
}
NumberCell {
Layout.preferredWidth: root.headerItem.holdingHeaderWidth
text: LocaleUtils.numberToLocaleString(model.amount)
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked(model.index, delegate, mouse)
}
}
}

View File

@ -0,0 +1,188 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQuick.Controls 2.15
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Popups 0.1
import utils 1.0
import shared.controls 1.0
Control {
id: root
// Expected roles: name, walletAddress, imageSource, amount
property var model
property string tokenName
property bool showRemotelyDestructMenuItem: true
readonly property alias sortBy: holdersList.sortBy
readonly property alias sorting: holdersList.sorting
signal viewProfileRequested(string address)
signal viewMessagesRequested(string address)
signal airdropRequested(string address)
signal remoteDestructRequested(string address)
signal kickRequested(string address)
signal banRequested(string address)
TokenHoldersProxyModel {
id: proxyModel
sourceModel: root.model
searchText: searcher.text
sortBy: holdersList.sortBy
sortOrder: holdersList.sorting === SortableTokenHoldersList.Sorting.Descending
? Qt.DescendingOrder : Qt.AscendingOrder
}
QtObject {
id: d
readonly property int red2Color: 4
}
contentItem: ColumnLayout {
anchors.fill: parent
spacing: 0
StatusBaseText {
Layout.fillWidth: true
wrapMode: Text.Wrap
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.baseColor1
text: qsTr("%1 token holders").arg(root.tokenName)
}
SearchBox {
id: searcher
Layout.fillWidth: true
Layout.topMargin: 12
visible: !root.empty
topPadding: 0
bottomPadding: 0
minimumHeight: 36 // by design
maximumHeight: minimumHeight
placeholderText: qsTr("Search hodlers")
}
StatusBaseText {
Layout.fillWidth: true
Layout.topMargin: 12
wrapMode: Text.Wrap
font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.baseColor1
visible: searcher.text.length > 0
text: (searcher.text.length > 0 && proxyModel.count > 0)
? qsTr("Search results") : qsTr("No hodlers found")
}
SortableTokenHoldersList {
id: holdersList
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
Layout.topMargin: 20
model: proxyModel
onClicked: {
if (mouse.button !== Qt.RightButton)
return
const entry = ModelUtils.get(proxyModel, index)
const address = entry.walletAddress
const name = entry.name
menu.rawAddress = name === ""
menu.currentAddress = address
menu.popup(parent, mouse.x, mouse.y)
holdersList.currentIndex = index
}
}
}
StatusMenu {
id: menu
property string currentAddress
property bool rawAddress
onClosed: holdersList.currentIndex = -1
StatusAction {
text: qsTr("View Profile")
icon.name: "profile"
enabled: !menu.rawAddress
onTriggered: root.viewProfileRequested(menu.currentAddress)
}
StatusAction {
text: qsTr("View Messages")
icon.name: "chat"
enabled: !menu.rawAddress
onTriggered: root.viewMessagesRequested(menu.currentAddress)
}
StatusAction {
text: qsTr("Airdrop")
icon.name: "airdrop"
onTriggered: root.airdropRequested(menu.currentAddress)
}
StatusMenuSeparator {
visible: remotelyDestructAction.enabled || kickAction.enabled
|| banAction.enabled
}
StatusAction {
id: remotelyDestructAction
text: qsTr("Remotely destruct")
icon.name: "destroy"
enabled: root.showRemotelyDestructMenuItem
type: StatusBaseButton.Type.Danger
onTriggered: root.remoteDestructRequested(menu.currentAddress)
}
StatusAction {
id: kickAction
text: qsTr("Kick")
icon.name: "warning"
enabled: !menu.rawAddress
type: StatusBaseButton.Type.Danger
onTriggered: root.kickRequested(menu.currentAddress)
}
StatusAction {
id: banAction
text: qsTr("Ban")
icon.name: "cancel"
enabled: !menu.rawAddress
type: StatusBaseButton.Type.Danger
onTriggered: root.banRequested(menu.currentAddress)
}
}
}

View File

@ -0,0 +1,63 @@
import SortFilterProxyModel 0.2
SortFilterProxyModel {
id: root
property string searchText
readonly property string searchTextLowerCase: searchText.toLowerCase()
property int sortBy: SortableTokenHoldersList.SortBy.Username
property int sortOrder: Qt.AscendingOrder
filters: ExpressionFilter {
expression: {
root.searchTextLowerCase
const nameLowerCase = model.name.toLowerCase()
const addressLowerCase = model.walletAddress.toLowerCase()
return nameLowerCase.includes(searchTextLowerCase) ||
addressLowerCase.includes(searchTextLowerCase)
}
}
sorters: [
FilterSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username
ValueFilter {
roleName: "name"
value: ""
inverted: true
}
priority: 3
},
RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username
roleName: "name"
sortOrder: root.sortOrder
priority: 2
},
RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username
roleName: "walletAddress"
sortOrder: root.sortOrder
priority: 1
},
RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.NoOfMessages
roleName: "noOfMessages"
sortOrder: root.sortOrder
},
RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Holding
roleName: "amount"
sortOrder: root.sortOrder
}
]
}

View File

@ -9,5 +9,8 @@ JoinPermissionsOverlayPanel 1.0 JoinPermissionsOverlayPanel.qml
MintTokensFooterPanel 1.0 MintTokensFooterPanel.qml
PermissionConflictWarningPanel 1.0 PermissionConflictWarningPanel.qml
PermissionQualificationPanel 1.0 PermissionQualificationPanel.qml
SortableTokenHoldersList 1.0 SortableTokenHoldersList.qml
SortableTokenHoldersPanel 1.0 SortableTokenHoldersPanel.qml
TokenHoldersPanel 1.0 TokenHoldersPanel.qml
TokenHoldersProxyModel 1.0 TokenHoldersProxyModel.qml
WarningPanel 1.0 WarningPanel.qml