fix(TokenHoldersPanel): Added sorting feature

Closes #11032
This commit is contained in:
Alexandra Betouni 2023-07-05 18:17:51 +03:00
parent ba89daa737
commit a1e1e316db
9 changed files with 379 additions and 163 deletions

View File

@ -34,8 +34,7 @@ SplitView {
sourceModel: tokenHoldersModel sourceModel: tokenHoldersModel
sortBy: holdersList.sortBy sortBy: holdersList.sortBy
sortOrder: holdersList.sorting === SortableTokenHoldersList.Sorting.Descending sortOrder: holdersList.sortOrder ? Qt.DescendingOrder : Qt.AscendingOrder
? Qt.DescendingOrder : Qt.AscendingOrder
} }
onClicked: logs.logEvent("holdersList.clicked: " + index) onClicked: logs.logEvent("holdersList.clicked: " + index)

View File

@ -20,10 +20,10 @@ SplitView {
Item { Item {
SplitView.fillWidth: true SplitView.fillWidth: true
SplitView.fillHeight: true SplitView.fillHeight: true
TokenHoldersPanel { TokenHoldersPanel {
anchors.centerIn: parent
width: 568 width: 568
height: 364
anchors.centerIn: parent
tokenName: "Aniversary" tokenName: "Aniversary"
model: TokenHoldersModel {} model: TokenHoldersModel {}
isSelectorMode: editorSelectorMode.checked isSelectorMode: editorSelectorMode.checked

View File

@ -33,7 +33,7 @@ ListModel {
}, },
{ {
name: "", name: "",
walletAddress: "0xb794f5ea0ba39494ce839613fffba74279579268", walletAddress: "0xb794f5ea0ba394782634hhh3fffba74279579264",
imageSource: "", imageSource: "",
amount: 1, amount: 1,
noOfMessages: 0, noOfMessages: 0,
@ -41,7 +41,7 @@ ListModel {
}, },
{ {
name: "", name: "",
walletAddress: "0xc794f5ea0ba39494ce839613fffba74279579268", walletAddress: "0xc794f577990jjjjjewaofherfffba74279579265",
imageSource: "", imageSource: "",
amount: 11, amount: 11,
noOfMessages: 0, noOfMessages: 0,
@ -49,7 +49,7 @@ ListModel {
}, },
{ {
name: "", name: "",
walletAddress: "0xd794f5ea0ba39494ce839613fffba74279579268", walletAddress: "0xd794f5ea009fnrsehggwe7777ffba74279579266",
imageSource: "", imageSource: "",
amount: 14, amount: 14,
noOfMessages: 0, noOfMessages: 0,

View File

@ -20,16 +20,8 @@ import utils 1.0
StatusListView { StatusListView {
id: root id: root
enum SortBy {
None, Username, NoOfMessages, Holding
}
enum Sorting {
Descending, Ascending
}
readonly property alias sortBy: d.sortBy 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) signal clicked(int index, var parent, var mouse)
@ -55,9 +47,9 @@ StatusListView {
d.resetOtherHeaders(this) d.resetOtherHeaders(this)
if (sorting === StatusSortableColumnHeader.Sorting.Ascending) if (sorting === StatusSortableColumnHeader.Sorting.Ascending)
d.sorting = SortableTokenHoldersList.Sorting.Ascending d.sorting = Qt.AscendingOrder
else if (sorting === StatusSortableColumnHeader.Sorting.Descending) else if (sorting === StatusSortableColumnHeader.Sorting.Descending)
d.sorting = SortableTokenHoldersList.Sorting.Descending d.sorting = Qt.DescendingOrder
} }
} }
@ -75,8 +67,8 @@ StatusListView {
QtObject { QtObject {
id: d id: d
property int sortBy: SortableTokenHoldersList.SortBy.None property int sortBy: TokenHoldersProxyModel.SortBy.None
property int sorting: SortableTokenHoldersList.Sorting.Descending property int sorting: Qt.DescendingOrder
readonly property int red2Color: 4 readonly property int red2Color: 4
@ -113,9 +105,9 @@ StatusListView {
onClicked: { onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting) if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = SortableTokenHoldersList.SortBy.Username d.sortBy = TokenHoldersProxyModel.SortBy.Username
else else
d.sortBy = SortableTokenHoldersList.SortBy.None d.sortBy = TokenHoldersProxyModel.SortBy.None
} }
} }
@ -131,9 +123,9 @@ StatusListView {
onClicked: { onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting) if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = SortableTokenHoldersList.SortBy.NoOfMessages d.sortBy = TokenHoldersProxyModel.SortBy.NoOfMessages
else else
d.sortBy = SortableTokenHoldersList.SortBy.None d.sortBy = TokenHoldersProxyModel.SortBy.None
} }
} }
@ -149,9 +141,9 @@ StatusListView {
onClicked: { onClicked: {
if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting) if (sorting !== StatusSortableColumnHeader.Sorting.NoSorting)
d.sortBy = SortableTokenHoldersList.SortBy.Holding d.sortBy = TokenHoldersProxyModel.SortBy.Holding
else else
d.sortBy = SortableTokenHoldersList.SortBy.None d.sortBy = TokenHoldersProxyModel.SortBy.None
} }
} }
} }
@ -185,7 +177,7 @@ StatusListView {
} }
readonly property bool showSeparator: isFirstRowAddress readonly property bool showSeparator: isFirstRowAddress
&& root.sortBy === SortableTokenHoldersList.SortBy.Username && root.sortBy === TokenHoldersProxyModel.SortBy.Username
width: ListView.view.width width: ListView.view.width

View File

@ -23,7 +23,7 @@ Control {
property alias isAirdropEnabled: infoBoxPanel.buttonEnabled property alias isAirdropEnabled: infoBoxPanel.buttonEnabled
readonly property alias sortBy: holdersList.sortBy 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 readonly property bool empty: countCheckHelper.count === 0
@ -50,8 +50,7 @@ Control {
searchText: searcher.text searchText: searcher.text
sortBy: holdersList.sortBy sortBy: holdersList.sortBy
sortOrder: holdersList.sorting === SortableTokenHoldersList.Sorting.Descending sortOrder: holdersList.sortOrder ? Qt.DescendingOrder : Qt.AscendingOrder
? Qt.DescendingOrder : Qt.AscendingOrder
} }
QtObject { QtObject {

View File

@ -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);
}
}
}
}
}
}
}

View File

@ -1,165 +1,97 @@
import QtQuick 2.14 import QtQuick 2.15
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.15
import QtQuick.Controls 2.14 import QtQuick.Controls 2.15
import StatusQ.Controls 0.1
import StatusQ.Core 0.1 import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1 import StatusQ.Core.Utils 0.1
import StatusQ.Components 0.1 import StatusQ.Popups 0.1
import SortFilterProxyModel 0.2
import utils 1.0 import utils 1.0
import shared.controls 1.0 import shared.controls 1.0
Control { Control {
id: root id: root
// Expected roles: name, walletAddress, imageSource, amount
property var model property var model
property string tokenName property string tokenName
property bool isSelectorMode: false property bool isSelectorMode: false
signal selfDestructAmountChanged(string walletAddress, int amount) signal selfDestructAmountChanged(string walletAddress, int amount)
signal selfDestructRemoved(string walletAddress) signal selfDestructRemoved(string walletAddress)
bottomPadding: 16
QtObject { TokenHoldersProxyModel {
id: d 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 { 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 { text: qsTr("%1 token holders").arg(root.tokenName)
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())
}
}
} }
SearchBox { SearchBox {
id: searcher id: searcher
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 12
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.padding
visible: !root.empty
topPadding: 0 topPadding: 0
bottomPadding: 0 bottomPadding: 0
minimumHeight: 36 // by design minimumHeight: 36 // by design
maximumHeight: minimumHeight maximumHeight: minimumHeight
enabled: root.model && root.model.count > 0 placeholderText: qsTr("Search hodlers")
placeholderText: enabled ? qsTr("Search") : qsTr("No placeholders to search")
} }
StatusBaseText { StatusBaseText {
id: anotherLabel
Layout.fillWidth: true Layout.fillWidth: true
Layout.topMargin: 12
Layout.leftMargin: Style.current.padding
Layout.rightMargin: Style.current.padding
visible: !root.preview
wrapMode: Text.Wrap wrapMode: Text.Wrap
font.pixelSize: Style.current.primaryTextFontSize font.pixelSize: Style.current.primaryTextFontSize
color: Theme.palette.baseColor1 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") : ""
} }
TokenHoldersList {
Item { id: holdersList
id: scrollViewWrapper
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
implicitWidth: scrollView.implicitWidth Layout.topMargin: 12
implicitHeight: scrollView.implicitHeight isSelectorMode: root.isSelectorMode
StatusListView {
id: scrollView
anchors.fill: parent
implicitHeight: contentHeight
model: filteredModel model: filteredModel
onSelfDestructRemoved: {
ScrollBar.vertical: StatusScrollBar { root.selfDestructRemoved(walletAddress);
parent: scrollViewWrapper
anchors.top: scrollView.top
anchors.bottom: scrollView.bottom
anchors.left: scrollView.right
anchors.leftMargin: 1
} }
onSelfDestructAmountChanged: {
delegate: RowLayout { root.selfDestructAmountChanged(walletAddress, amount);
width: ListView.view.width }
spacing: Style.current.padding }
Rectangle {
StatusListItem {
readonly property bool unknownHolder: model.name === ""
readonly property string formattedTitle: unknownHolder ? "?" : model.name
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 4
leftPadding: 0 Layout.alignment: Qt.AlignBottom
rightPadding: 0 color: Theme.palette.baseColor2
sensor.enabled: false opacity: holdersList.bottomSeparatorVisible ? 1.0 : 0.0
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)
}
}
}
}
} }
} }
} }

View File

@ -6,9 +6,13 @@ SortFilterProxyModel {
property string searchText property string searchText
readonly property string searchTextLowerCase: searchText.toLowerCase() readonly property string searchTextLowerCase: searchText.toLowerCase()
property int sortBy: SortableTokenHoldersList.SortBy.Username property int sortBy: TokenHoldersProxyModel.SortBy.Username
property int sortOrder: Qt.AscendingOrder property int sortOrder: Qt.AscendingOrder
enum SortBy {
None, Username, NoOfMessages, Holding
}
filters: ExpressionFilter { filters: ExpressionFilter {
expression: { expression: {
root.searchTextLowerCase root.searchTextLowerCase
@ -23,7 +27,7 @@ SortFilterProxyModel {
sorters: [ sorters: [
FilterSorter { FilterSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Username
ValueFilter { ValueFilter {
roleName: "name" roleName: "name"
@ -35,27 +39,27 @@ SortFilterProxyModel {
}, },
RoleSorter { RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Username
roleName: "name" roleName: "name"
sortOrder: root.sortOrder sortOrder: root.sortOrder
priority: 2 priority: 2
}, },
RoleSorter { RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Username enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Username
roleName: "walletAddress" roleName: "walletAddress"
sortOrder: root.sortOrder sortOrder: root.sortOrder
priority: 1 priority: 1
}, },
RoleSorter { RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.NoOfMessages enabled: root.sortBy === TokenHoldersProxyModel.SortBy.NoOfMessages
roleName: "noOfMessages" roleName: "noOfMessages"
sortOrder: root.sortOrder sortOrder: root.sortOrder
}, },
RoleSorter { RoleSorter {
enabled: root.sortBy === SortableTokenHoldersList.SortBy.Holding enabled: root.sortBy === TokenHoldersProxyModel.SortBy.Holding
roleName: "amount" roleName: "amount"
sortOrder: root.sortOrder sortOrder: root.sortOrder
} }

View File

@ -63,14 +63,10 @@ StatusDialog {
implicitWidth: 600 // by design implicitWidth: 600 // by design
padding: 0 padding: 0
TokenHoldersPanel { contentItem: TokenHoldersPanel {
id: tokenHoldersPanel id: tokenHoldersPanel
anchors.fill: parent
padding: 16
tokenName: root.collectibleName tokenName: root.collectibleName
isSelectorMode: true isSelectorMode: true
onSelfDestructAmountChanged: d.updateTokensToDestruct(walletAddress, amount) onSelfDestructAmountChanged: d.updateTokensToDestruct(walletAddress, amount)
onSelfDestructRemoved: d.clearTokensToDesctruct(walletAddress) onSelfDestructRemoved: d.clearTokensToDesctruct(walletAddress)
} }
@ -78,6 +74,12 @@ StatusDialog {
footer: StatusDialogFooter { footer: StatusDialogFooter {
spacing: Style.current.padding spacing: Style.current.padding
rightButtons: ObjectModel { rightButtons: ObjectModel {
StatusFlatButton {
text: qsTr("Cancel")
onClicked: {
root.close()
}
}
StatusButton { StatusButton {
enabled: d.tokenCount > 0 enabled: d.tokenCount > 0
text: qsTr("Remotely destruct %n token(s)", "", d.tokenCount) text: qsTr("Remotely destruct %n token(s)", "", d.tokenCount)