diff --git a/storybook/pages/SortableTokenHoldersListPage.qml b/storybook/pages/SortableTokenHoldersListPage.qml index 05daea7000..c987bc63b1 100644 --- a/storybook/pages/SortableTokenHoldersListPage.qml +++ b/storybook/pages/SortableTokenHoldersListPage.qml @@ -34,8 +34,7 @@ SplitView { sourceModel: tokenHoldersModel sortBy: holdersList.sortBy - sortOrder: holdersList.sorting === SortableTokenHoldersList.Sorting.Descending - ? Qt.DescendingOrder : Qt.AscendingOrder + sortOrder: holdersList.sortOrder ? Qt.DescendingOrder : Qt.AscendingOrder } onClicked: logs.logEvent("holdersList.clicked: " + index) diff --git a/storybook/pages/TokenHoldersPanelPage.qml b/storybook/pages/TokenHoldersPanelPage.qml index 8e6b7f6b5e..4adabdf155 100644 --- a/storybook/pages/TokenHoldersPanelPage.qml +++ b/storybook/pages/TokenHoldersPanelPage.qml @@ -20,10 +20,10 @@ SplitView { Item { SplitView.fillWidth: true SplitView.fillHeight: true - TokenHoldersPanel { - anchors.centerIn: parent width: 568 + height: 364 + anchors.centerIn: parent tokenName: "Aniversary" model: TokenHoldersModel {} isSelectorMode: editorSelectorMode.checked diff --git a/storybook/src/Models/TokenHoldersModel.qml b/storybook/src/Models/TokenHoldersModel.qml index 21e4ae360d..cfc87ed221 100644 --- a/storybook/src/Models/TokenHoldersModel.qml +++ b/storybook/src/Models/TokenHoldersModel.qml @@ -33,7 +33,7 @@ ListModel { }, { name: "", - walletAddress: "0xb794f5ea0ba39494ce839613fffba74279579268", + walletAddress: "0xb794f5ea0ba394782634hhh3fffba74279579264", imageSource: "", amount: 1, noOfMessages: 0, @@ -41,7 +41,7 @@ ListModel { }, { name: "", - walletAddress: "0xc794f5ea0ba39494ce839613fffba74279579268", + walletAddress: "0xc794f577990jjjjjewaofherfffba74279579265", imageSource: "", amount: 11, noOfMessages: 0, @@ -49,7 +49,7 @@ ListModel { }, { name: "", - walletAddress: "0xd794f5ea0ba39494ce839613fffba74279579268", + walletAddress: "0xd794f5ea009fnrsehggwe7777ffba74279579266", imageSource: "", amount: 14, noOfMessages: 0, diff --git a/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersList.qml b/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersList.qml index 981bb5fdee..a2162cfa5f 100644 --- a/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersList.qml +++ b/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersList.qml @@ -20,16 +20,8 @@ import utils 1.0 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 + readonly property alias sortOrder: d.sorting signal clicked(int index, var parent, var mouse) @@ -55,9 +47,9 @@ StatusListView { d.resetOtherHeaders(this) if (sorting === StatusSortableColumnHeader.Sorting.Ascending) - d.sorting = SortableTokenHoldersList.Sorting.Ascending + d.sorting = Qt.AscendingOrder else if (sorting === StatusSortableColumnHeader.Sorting.Descending) - d.sorting = SortableTokenHoldersList.Sorting.Descending + d.sorting = Qt.DescendingOrder } } @@ -75,8 +67,8 @@ StatusListView { QtObject { id: d - property int sortBy: SortableTokenHoldersList.SortBy.None - property int sorting: SortableTokenHoldersList.Sorting.Descending + property int sortBy: TokenHoldersProxyModel.SortBy.None + property int sorting: Qt.DescendingOrder readonly property int red2Color: 4 @@ -113,9 +105,9 @@ StatusListView { onClicked: { if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting) - d.sortBy = SortableTokenHoldersList.SortBy.Username + d.sortBy = TokenHoldersProxyModel.SortBy.Username else - d.sortBy = SortableTokenHoldersList.SortBy.None + d.sortBy = TokenHoldersProxyModel.SortBy.None } } @@ -131,9 +123,9 @@ StatusListView { onClicked: { if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting) - d.sortBy = SortableTokenHoldersList.SortBy.NoOfMessages + d.sortBy = TokenHoldersProxyModel.SortBy.NoOfMessages else - d.sortBy = SortableTokenHoldersList.SortBy.None + d.sortBy = TokenHoldersProxyModel.SortBy.None } } @@ -149,9 +141,9 @@ StatusListView { onClicked: { if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting) - d.sortBy = SortableTokenHoldersList.SortBy.Holding + d.sortBy = TokenHoldersProxyModel.SortBy.Holding else - d.sortBy = SortableTokenHoldersList.SortBy.None + d.sortBy = TokenHoldersProxyModel.SortBy.None } } } @@ -185,7 +177,7 @@ StatusListView { } readonly property bool showSeparator: isFirstRowAddress - && root.sortBy === SortableTokenHoldersList.SortBy.Username + && root.sortBy === TokenHoldersProxyModel.SortBy.Username width: ListView.view.width diff --git a/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersPanel.qml b/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersPanel.qml index be4b259cbc..ba26543309 100644 --- a/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/SortableTokenHoldersPanel.qml @@ -23,7 +23,7 @@ Control { property alias isAirdropEnabled: infoBoxPanel.buttonEnabled readonly property alias sortBy: holdersList.sortBy - readonly property alias sorting: holdersList.sorting + readonly property alias sorting: holdersList.sortOrder readonly property bool empty: countCheckHelper.count === 0 @@ -50,8 +50,7 @@ Control { searchText: searcher.text sortBy: holdersList.sortBy - sortOrder: holdersList.sorting === SortableTokenHoldersList.Sorting.Descending - ? Qt.DescendingOrder : Qt.AscendingOrder + sortOrder: holdersList.sortOrder ? Qt.DescendingOrder : Qt.AscendingOrder } QtObject { diff --git a/ui/app/AppLayouts/Communities/panels/TokenHoldersList.qml b/ui/app/AppLayouts/Communities/panels/TokenHoldersList.qml new file mode 100644 index 0000000000..a49761d088 --- /dev/null +++ b/ui/app/AppLayouts/Communities/panels/TokenHoldersList.qml @@ -0,0 +1,288 @@ +import QtQuick 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Controls 2.14 + +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 + +import SortFilterProxyModel 0.2 + +import utils 1.0 +import shared.controls 1.0 + +/*! + \qmltype TokenHoldersList + \inherits StatusListView + \brief Shows list of users or addresses with corrensponding numbers of + messages and holding amounts. + + Expected roles: name, walletAddress, imageSource, amount + */ + + +Item { + id: root + implicitHeight: (listView.contentHeight+header.height+12)//initial height plus top margin + + property alias model: listView.model + property bool isSelectorMode: false + readonly property alias sortBy: d.sortBy + readonly property alias sortOrder: d.sorting + readonly property bool bottomSeparatorVisible: ((listView.contentY > 0) && + (listView.contentY < (listView.contentHeight - listView.height - 40/*margins*/))) + + signal selfDestructAmountChanged(string walletAddress, int amount) + signal selfDestructRemoved(string walletAddress) + + QtObject { + id: d + property int sortBy: TokenHoldersProxyModel.SortBy.None + property int sorting: Qt.DescendingOrder + property var selectedTokenAmount: new Map(); + property var selectedTokenChecked: new Map(); + property int delegateHeight: 64 + + signal resetOtherHeaders(var header) + } + + clip: true + + Control { + id: header + width: parent.width + height: 40 + readonly property alias usernameHeaderWidth: usernameHeader.width + readonly property alias holdingHeaderWidth: holdingHeader.width + background: Rectangle { + id: scrollingSeparator + width: parent.width + height: 4 + anchors.bottom: parent.bottom + color: Theme.palette.baseColor2 + visible: (listView.contentY > 0) + } + contentItem: Item { + anchors.fill: parent + anchors.leftMargin: Style.current.padding + anchors.rightMargin: Style.current.padding + clip: true + RowLayout { + id: row + anchors.fill: parent + 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 = TokenHoldersProxyModel.SortBy.Username + else + d.sortBy = TokenHoldersProxyModel.SortBy.None + } + } + Item { Layout.fillWidth: true } + } + + ColumnHeader { + id: holdingHeader + text: qsTr("Hodling") + onClicked: { + if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting) + d.sortBy = TokenHoldersProxyModel.SortBy.Holding + else + d.sortBy = TokenHoldersProxyModel.SortBy.None + } + } + Item { + Layout.preferredWidth: 233 + Layout.rightMargin: Style.current.halfPadding + } + } + } + } + + StatusListView { + id: listView + anchors.fill: parent + anchors.topMargin: header.height + currentIndex: -1 + component ColumnHeader: StatusSortableColumnHeader { + id: columnHeader + + leftPadding: 0 + rightPadding: 0 + Connections { + target: d + + function onResetOtherHeaders(header) { + if (header !== columnHeader) + columnHeader.reset() + } + } + + onClicked: { + d.resetOtherHeaders(this) + + if (sorting === StatusSortableColumnHeader.Sorting.Ascending) + d.sorting = Qt.AscendingOrder + else if (sorting === StatusSortableColumnHeader.Sorting.Descending) + d.sorting = Qt.DescendingOrder + } + } + + component NumberCell: StatusBaseText { + horizontalAlignment: Qt.AlignRight + + font.weight: Font.Medium + font.pixelSize: 13 + + color: Theme.palette.baseColor1 + elide: Qt.ElideRight + } + + delegate: ItemDelegate { + id: delegate + width: ListView.view.width + height: d.delegateHeight + padding: 0 + readonly property string name: model.name + + readonly property bool isFirstRowAddress: { + if (model.name !== "") + return false + + const item = listView.itemAtIndex(index - 1) + return item && item.name + } + + readonly property bool showSeparator: isFirstRowAddress + && root.sortBy === TokenHoldersProxyModel.SortBy.Username + + + background: Item { + Rectangle { + anchors.fill: parent + 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 { + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Style.current.padding + anchors.rightMargin: Style.current.padding + RowLayout { + spacing: Style.current.halfPadding + + StatusListItem { + readonly property bool unknownHolder: model.name === "" + readonly property string formattedTitle: unknownHolder ? "?" : model.name + Layout.preferredWidth: header.usernameHeaderWidth + color: "transparent" + leftPadding: 0 + rightPadding: 0 + sensor.enabled: false + title: formattedTitle + statusListItemTitle.visible: !unknownHolder + subTitle: Utils.getElidedPk(model.walletAddress) + asset.name: model.imageSource + asset.isImage: true + asset.isLetterIdenticon: !asset.name + asset.color: Theme.palette.getColor("red2") + } + + NumberCell { + Layout.preferredWidth: header.holdingHeaderWidth + Layout.leftMargin: Style.current.halfPadding + + text: LocaleUtils.numberToLocaleString(model.amount) + } + + Item { Layout.preferredWidth: 100 } + + StatusComboBox { + id: combo + Layout.preferredWidth: 68 + Layout.preferredHeight: 44 + control.spacing: Style.current.halfPadding / 2 + model: amount + size: StatusComboBox.Size.Small + type: StatusComboBox.Type.Secondary + delegate: StatusItemDelegate { + width: combo.control.width + centerTextHorizontally: true + highlighted: combo.control.highlightedIndex === index + font: combo.control.font + text: Number(modelData) + 1 + } + contentItem: StatusBaseText { + id: comboText + font: combo.control.font + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + color: Theme.palette.baseColor1 + Component.onCompleted: { + if (d.selectedTokenAmount.get(walletAddress) === undefined) { + d.selectedTokenAmount.set(walletAddress, amount); + } + text = d.selectedTokenAmount.get(walletAddress); + } + } + + control.onActivated: { + d.selectedTokenAmount.set(walletAddress, (index+1)); + comboText.text = d.selectedTokenAmount.get(walletAddress); + if (checkBox.checked) { + root.selfDestructAmountChanged(walletAddress, d.selectedTokenAmount.get(walletAddress)) + } + } + } + Item { Layout.preferredWidth: 28 } + StatusCheckBox { + id: checkBox + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 + Layout.alignment: Qt.AlignRight + visible: root.isSelectorMode + padding: 0 + onCheckStateChanged: { + if (checked) + root.selfDestructAmountChanged(model.walletAddress, d.selectedTokenAmount.get(walletAddress)) + else + root.selfDestructRemoved(model.walletAddress) + + d.selectedTokenChecked.set(walletAddress, checked); + } + Component.onCompleted: { + checked = !!d.selectedTokenChecked.get(walletAddress); + } + } + } + } + } + } +} diff --git a/ui/app/AppLayouts/Communities/panels/TokenHoldersPanel.qml b/ui/app/AppLayouts/Communities/panels/TokenHoldersPanel.qml index e77a002f81..c56181a5f4 100644 --- a/ui/app/AppLayouts/Communities/panels/TokenHoldersPanel.qml +++ b/ui/app/AppLayouts/Communities/panels/TokenHoldersPanel.qml @@ -1,165 +1,97 @@ -import QtQuick 2.14 -import QtQuick.Layouts 1.14 -import QtQuick.Controls 2.14 - +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.Controls 0.1 -import StatusQ.Components 0.1 - -import SortFilterProxyModel 0.2 +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 isSelectorMode: false signal selfDestructAmountChanged(string walletAddress, int amount) signal selfDestructRemoved(string walletAddress) + bottomPadding: 16 - QtObject { - id: d + TokenHoldersProxyModel { + id: filteredModel + sourceModel: root.model + searchText: searcher.text - readonly property int red2Color: 4 + sortBy: holdersList.sortBy + sortOrder: holdersList.sortOrder ? Qt.DescendingOrder : Qt.AscendingOrder } contentItem: ColumnLayout { - spacing: Style.current.padding + id: column + anchors.fill: parent + anchors.topMargin: Style.current.padding + spacing: 0 + StatusBaseText { + id: txtLabel + Layout.fillWidth: true + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding + wrapMode: Text.Wrap + font.pixelSize: Style.current.primaryTextFontSize + color: Theme.palette.baseColor1 - SortFilterProxyModel { - id: filteredModel - - sourceModel: root.model - filters: ExpressionFilter { - enabled: searcher.enabled - expression: { - searcher.text - return model.name.toLowerCase().includes(searcher.text.toLowerCase()) || - model.walletAddress.toLowerCase().includes(searcher.text.toLowerCase()) - } - } + text: qsTr("%1 token holders").arg(root.tokenName) } SearchBox { id: searcher - Layout.fillWidth: true - + Layout.topMargin: 12 + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding + visible: !root.empty topPadding: 0 bottomPadding: 0 minimumHeight: 36 // by design maximumHeight: minimumHeight - enabled: root.model && root.model.count > 0 - placeholderText: enabled ? qsTr("Search") : qsTr("No placeholders to search") + placeholderText: qsTr("Search hodlers") } - StatusBaseText { + id: anotherLabel Layout.fillWidth: true + Layout.topMargin: 12 + Layout.leftMargin: Style.current.padding + Layout.rightMargin: Style.current.padding - visible: !root.preview wrapMode: Text.Wrap font.pixelSize: Style.current.primaryTextFontSize color: Theme.palette.baseColor1 - text: searcher.text.length > 0 ? qsTr("Search results") : qsTr("All %1 token holders").arg(root.tokenName) + visible: (searcher.text.length > 0 && filteredModel.count === 0) + text: visible ? qsTr("No hodlers found") : "" } - - Item { - id: scrollViewWrapper + TokenHoldersList { + id: holdersList Layout.fillWidth: true Layout.fillHeight: true - implicitWidth: scrollView.implicitWidth - implicitHeight: scrollView.implicitHeight - - StatusListView { - id: scrollView - - anchors.fill: parent - implicitHeight: contentHeight - - model: filteredModel - - ScrollBar.vertical: StatusScrollBar { - parent: scrollViewWrapper - anchors.top: scrollView.top - anchors.bottom: scrollView.bottom - anchors.left: scrollView.right - anchors.leftMargin: 1 - } - - delegate: RowLayout { - width: ListView.view.width - spacing: Style.current.padding - - StatusListItem { - readonly property bool unknownHolder: model.name === "" - readonly property string formattedTitle: unknownHolder ? "?" : model.name - - Layout.fillWidth: true - - leftPadding: 0 - rightPadding: 0 - sensor.enabled: false - title: formattedTitle - statusListItemTitle.visible: !unknownHolder - subTitle: model.walletAddress - asset.name: model.imageSource - asset.isImage: true - asset.isLetterIdenticon: unknownHolder - asset.color: Theme.palette.userCustomizationColors[d.red2Color] - } - - StatusComboBox { - id: combo - Layout.preferredWidth: 88 - Layout.preferredHeight: 44 - visible: root.isSelectorMode && amount > 1 - control.spacing: Style.current.halfPadding / 2 - model: amount - size: StatusComboBox.Size.Small - type: StatusComboBox.Type.Secondary - delegate: StatusItemDelegate { - width: combo.control.width - centerTextHorizontally: true - highlighted: combo.control.highlightedIndex === index - font: combo.control.font - text: Number(modelData) + 1 - } - contentItem: StatusBaseText { - font: combo.control.font - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - text: Number(combo.control.displayText) + 1 - color: Theme.palette.baseColor1 - } - - control.onDisplayTextChanged: { - if(checkBox.checked) - root.selfDestructAmountChanged(walletAddress, Number(combo.currentIndex) + 1) - } - } - - StatusCheckBox { - id: checkBox - - Layout.leftMargin: Style.current.padding - visible: root.isSelectorMode - padding: 0 - onCheckStateChanged: { - if(checked) - root.selfDestructAmountChanged(model.walletAddress, Number(combo.currentIndex) + 1) - else - root.selfDestructRemoved(model.walletAddress) - } - } - } + Layout.topMargin: 12 + isSelectorMode: root.isSelectorMode + model: filteredModel + onSelfDestructRemoved: { + root.selfDestructRemoved(walletAddress); } + onSelfDestructAmountChanged: { + root.selfDestructAmountChanged(walletAddress, amount); + } + } + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 4 + Layout.alignment: Qt.AlignBottom + color: Theme.palette.baseColor2 + opacity: holdersList.bottomSeparatorVisible ? 1.0 : 0.0 } } } diff --git a/ui/app/AppLayouts/Communities/panels/TokenHoldersProxyModel.qml b/ui/app/AppLayouts/Communities/panels/TokenHoldersProxyModel.qml index 1bf005f978..3018bc8a1a 100644 --- a/ui/app/AppLayouts/Communities/panels/TokenHoldersProxyModel.qml +++ b/ui/app/AppLayouts/Communities/panels/TokenHoldersProxyModel.qml @@ -6,9 +6,13 @@ SortFilterProxyModel { property string searchText readonly property string searchTextLowerCase: searchText.toLowerCase() - property int sortBy: SortableTokenHoldersList.SortBy.Username + property int sortBy: TokenHoldersProxyModel.SortBy.Username property int sortOrder: Qt.AscendingOrder + enum SortBy { + None, Username, NoOfMessages, Holding + } + filters: ExpressionFilter { expression: { root.searchTextLowerCase @@ -23,7 +27,7 @@ SortFilterProxyModel { sorters: [ FilterSorter { - enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username + enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Username ValueFilter { roleName: "name" @@ -35,27 +39,27 @@ SortFilterProxyModel { }, RoleSorter { - enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username + enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Username roleName: "name" sortOrder: root.sortOrder priority: 2 }, RoleSorter { - enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username + enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Username roleName: "walletAddress" sortOrder: root.sortOrder priority: 1 }, RoleSorter { - enabled: root.sortBy === SortableTokenHoldersList.SortBy.NoOfMessages + enabled: root.sortBy === TokenHoldersProxyModel.SortBy.NoOfMessages roleName: "noOfMessages" sortOrder: root.sortOrder }, RoleSorter { - enabled: root.sortBy === SortableTokenHoldersList.SortBy.Holding + enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Holding roleName: "amount" sortOrder: root.sortOrder } diff --git a/ui/app/AppLayouts/Communities/popups/RemotelyDestructPopup.qml b/ui/app/AppLayouts/Communities/popups/RemotelyDestructPopup.qml index ef22b93b2e..9c297eadc6 100644 --- a/ui/app/AppLayouts/Communities/popups/RemotelyDestructPopup.qml +++ b/ui/app/AppLayouts/Communities/popups/RemotelyDestructPopup.qml @@ -63,14 +63,10 @@ StatusDialog { implicitWidth: 600 // by design padding: 0 - TokenHoldersPanel { + contentItem: TokenHoldersPanel { id: tokenHoldersPanel - - anchors.fill: parent - padding: 16 tokenName: root.collectibleName isSelectorMode: true - onSelfDestructAmountChanged: d.updateTokensToDestruct(walletAddress, amount) onSelfDestructRemoved: d.clearTokensToDesctruct(walletAddress) } @@ -78,6 +74,12 @@ StatusDialog { footer: StatusDialogFooter { spacing: Style.current.padding rightButtons: ObjectModel { + StatusFlatButton { + text: qsTr("Cancel") + onClicked: { + root.close() + } + } StatusButton { enabled: d.tokenCount > 0 text: qsTr("Remotely destruct %n token(s)", "", d.tokenCount)