596 lines
21 KiB
QML
596 lines
21 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Layouts 1.15
|
|
import QtQuick.Controls 2.15
|
|
import Qt.labs.settings 1.1
|
|
import QtQml 2.15
|
|
|
|
import StatusQ 0.1
|
|
import StatusQ.Components 0.1
|
|
import StatusQ.Controls 0.1
|
|
import StatusQ.Core 0.1
|
|
import StatusQ.Core.Theme 0.1
|
|
import StatusQ.Core.Utils 0.1
|
|
import StatusQ.Models 0.1
|
|
import StatusQ.Popups 0.1
|
|
import StatusQ.Popups.Dialog 0.1
|
|
|
|
import shared.controls 1.0
|
|
import shared.panels 1.0
|
|
import shared.popups 1.0
|
|
|
|
import utils 1.0
|
|
|
|
import AppLayouts.Wallet.views.collectibles 1.0
|
|
import AppLayouts.Wallet.controls 1.0
|
|
|
|
import SortFilterProxyModel 0.2
|
|
|
|
ColumnLayout {
|
|
id: root
|
|
|
|
required property var controller
|
|
required property string addressFilters
|
|
required property string networkFilters
|
|
property bool sendEnabled: true
|
|
property bool filterVisible
|
|
property bool isFetching: false // Indicates if a collectibles page is being loaded from the backend
|
|
property bool isUpdating: false // Indicates if the collectibles list is being updated
|
|
property bool isError: false // Indicates an error occurred while updating/fetching the collectibles list
|
|
|
|
signal collectibleClicked(int chainId, string contractAddress, string tokenId, string uid, int tokenType)
|
|
signal sendRequested(string symbol, int tokenType)
|
|
signal receiveRequested(string symbol)
|
|
signal switchToCommunityRequested(string communityId)
|
|
signal manageTokensRequested()
|
|
|
|
spacing: 0
|
|
|
|
QtObject {
|
|
id: d
|
|
|
|
readonly property int cellHeight: 225
|
|
readonly property int communityCellHeight: 242
|
|
readonly property int cellWidth: 176
|
|
readonly property int headerHeight: 56
|
|
|
|
readonly property bool isCustomView: cmbTokenOrder.currentValue === SortOrderComboBox.TokenOrderCustom
|
|
|
|
readonly property var sourceModel: root.controller.sourceModel
|
|
readonly property bool isLoading: root.isUpdating || root.isFetching
|
|
|
|
onIsLoadingChanged: {
|
|
d.loadingItemsModel.refresh()
|
|
}
|
|
|
|
readonly property var loadingItemsModel: ListModel {
|
|
Component.onCompleted: {
|
|
refresh()
|
|
}
|
|
|
|
function refresh() {
|
|
clear()
|
|
if (d.isLoading) {
|
|
for (let i = 0; i < 10; i++) {
|
|
append({ isLoading: true })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
readonly property var communityModel: CustomSFPM {
|
|
isCommunity: true
|
|
}
|
|
|
|
readonly property var communityModelWithLoadingItems: ConcatModel {
|
|
sources: [
|
|
SourceModel {
|
|
model: d.communityModel
|
|
markerRoleValue: "communityModel"
|
|
},
|
|
SourceModel {
|
|
model: d.loadingItemsModel
|
|
markerRoleValue: "loadingItemsModel"
|
|
}
|
|
]
|
|
|
|
markerRoleName: "sourceGroup"
|
|
}
|
|
|
|
readonly property var nonCommunityModel: CustomSFPM {
|
|
isCommunity: false
|
|
}
|
|
|
|
readonly property var nonCommunityModelWithLoadingItems: ConcatModel {
|
|
sources: [
|
|
SourceModel {
|
|
model: d.nonCommunityModel
|
|
markerRoleValue: "nonCommunityModel"
|
|
},
|
|
SourceModel {
|
|
model: d.loadingItemsModel
|
|
markerRoleValue: "loadingItemsModel"
|
|
}
|
|
]
|
|
|
|
markerRoleName: "sourceGroup"
|
|
}
|
|
|
|
readonly property bool hasCollectibles: d.nonCommunityModel.count || d.loadingItemsModel.count
|
|
readonly property bool hasCommunityCollectibles: d.communityModel.count || d.loadingItemsModel.count
|
|
|
|
readonly property bool onlyOneType: !hasCollectibles || !hasCommunityCollectibles
|
|
|
|
readonly property var nwFilters: root.networkFilters.split(":")
|
|
readonly property var addrFilters: root.addressFilters.split(":").map((addr) => addr.toLowerCase())
|
|
|
|
function getLatestTimestmap(ownership, filterList) {
|
|
let latest = 0
|
|
|
|
if (!!ownership) {
|
|
for (let i = 0; i < ownership.count; i++) {
|
|
let accountAddress = ModelUtils.get(ownership, i, "accountAddress").toLowerCase()
|
|
if (filterList.includes(accountAddress)) {
|
|
let txTimestamp = ModelUtils.get(ownership, i, "txTimestamp")
|
|
latest = Math.max(latest, txTimestamp)
|
|
}
|
|
}
|
|
}
|
|
return latest
|
|
}
|
|
|
|
function getBalance(ownership, filterList) {
|
|
// Balance is a Uint256, so we need to use AmountsArithmetic to handle it
|
|
let balance = AmountsArithmetic.fromNumber(0)
|
|
|
|
if (!!ownership) {
|
|
for (let i = 0; i < ownership.count; i++) {
|
|
let accountAddress = ModelUtils.get(ownership, i, "accountAddress").toLowerCase()
|
|
if (filterList.includes(accountAddress)) {
|
|
let tokenBalanceStr = ModelUtils.get(ownership, i, "balance")+""
|
|
if (tokenBalanceStr !== "") {
|
|
let tokenBalance = AmountsArithmetic.fromString(tokenBalanceStr)
|
|
balance = AmountsArithmetic.sum(balance, tokenBalance)
|
|
}
|
|
}
|
|
}
|
|
// For simplicity, we limit the result to the maximum int manageable by QML
|
|
const maxInt = 2147483647
|
|
if (AmountsArithmetic.cmp(balance, AmountsArithmetic.fromNumber(maxInt)) === 1) {
|
|
return maxInt
|
|
}
|
|
}
|
|
return AmountsArithmetic.toNumber(balance)
|
|
}
|
|
}
|
|
|
|
component CustomSFPM: SortFilterProxyModel {
|
|
id: customFilter
|
|
property bool isCommunity
|
|
|
|
sourceModel: d.sourceModel
|
|
proxyRoles: [
|
|
JoinRole {
|
|
name: "groupName"
|
|
roleNames: ["collectionName", "communityName"]
|
|
},
|
|
FastExpressionRole {
|
|
name: "balance"
|
|
expression: d.addrFilters, d.getBalance(model.ownership, d.addrFilters)
|
|
expectedRoles: ["ownership"]
|
|
},
|
|
FastExpressionRole {
|
|
name: "lastTxTimestamp"
|
|
expression: d.addrFilters, d.getLatestTimestmap(model.ownership, d.addrFilters)
|
|
expectedRoles: ["ownership"]
|
|
}
|
|
]
|
|
filters: [
|
|
FastExpressionFilter {
|
|
expression: {
|
|
return d.nwFilters.includes(model.chainId+"")
|
|
}
|
|
expectedRoles: ["chainId"]
|
|
},
|
|
ValueFilter {
|
|
roleName: "balance"
|
|
value: 0
|
|
inverted: true
|
|
},
|
|
FastExpressionFilter {
|
|
expression: {
|
|
root.controller.revision
|
|
return root.controller.filterAcceptsSymbol(model.symbol) && (customFilter.isCommunity ? !!model.communityId : !model.communityId)
|
|
}
|
|
expectedRoles: ["symbol", "communityId"]
|
|
},
|
|
FastExpressionFilter {
|
|
enabled: customFilter.isCommunity && cmbFilter.hasEnabledFilters
|
|
expression: cmbFilter.selectedFilterGroupIds.includes(model.communityId) ||
|
|
(!model.communityId && cmbFilter.selectedFilterGroupIds.includes(""))
|
|
expectedRoles: ["communityId"]
|
|
},
|
|
FastExpressionFilter {
|
|
enabled: !customFilter.isCommunity && cmbFilter.hasEnabledFilters
|
|
expression: cmbFilter.selectedFilterGroupIds.includes(model.collectionUid) ||
|
|
(!model.collectionUid && cmbFilter.selectedFilterGroupIds.includes(""))
|
|
expectedRoles: ["collectionUid"]
|
|
}
|
|
]
|
|
sorters: [
|
|
FastExpressionSorter {
|
|
expression: {
|
|
root.controller.revision
|
|
return root.controller.compareTokens(modelLeft.symbol, modelRight.symbol)
|
|
}
|
|
enabled: d.isCustomView
|
|
expectedRoles: ["symbol"]
|
|
},
|
|
RoleSorter {
|
|
roleName: cmbTokenOrder.currentSortRoleName
|
|
sortOrder: cmbTokenOrder.currentSortOrder
|
|
enabled: !d.isCustomView
|
|
}
|
|
]
|
|
}
|
|
|
|
Settings {
|
|
id: settings
|
|
category: "CollectiblesViewSortSettings"
|
|
property int currentSortValue: SortOrderComboBox.TokenOrderDateAdded
|
|
property alias currentSortOrder: cmbTokenOrder.currentSortOrder
|
|
property alias selectedFilterGroupIds: cmbFilter.selectedFilterGroupIds
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
settings.sync()
|
|
cmbTokenOrder.currentIndex = cmbTokenOrder.indexOfValue(settings.currentSortValue)
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
settings.currentSortValue = cmbTokenOrder.currentValue
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: false
|
|
Layout.preferredHeight: root.filterVisible ? implicitHeight : 0
|
|
spacing: 20
|
|
opacity: root.filterVisible ? 1 : 0
|
|
|
|
Behavior on Layout.preferredHeight { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
|
|
Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } }
|
|
|
|
StatusDialogDivider {
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: Style.current.halfPadding
|
|
|
|
FilterComboBox {
|
|
id: cmbFilter
|
|
regularTokensModel: root.controller.regularTokensModel
|
|
collectionGroupsModel: root.controller.collectionGroupsModel
|
|
communityTokenGroupsModel: root.controller.communityTokenGroupsModel
|
|
hasCommunityGroups: d.hasCommunityCollectibles
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.preferredHeight: 34
|
|
Layout.preferredWidth: 1
|
|
Layout.leftMargin: 12
|
|
Layout.rightMargin: 12
|
|
color: Theme.palette.baseColor2
|
|
}
|
|
|
|
StatusBaseText {
|
|
color: Theme.palette.baseColor1
|
|
font.pixelSize: Style.current.additionalTextSize
|
|
text: qsTr("Sort by:")
|
|
}
|
|
|
|
SortOrderComboBox {
|
|
id: cmbTokenOrder
|
|
hasCustomOrderDefined: root.controller.hasSettings
|
|
model: [
|
|
{ value: SortOrderComboBox.TokenOrderDateAdded, text: qsTr("Date added"), icon: "calendar", sortRoleName: "lastTxTimestamp" }, // Custom SFPM role
|
|
{ value: SortOrderComboBox.TokenOrderAlpha, text: qsTr("Collectible name"), icon: "bold", sortRoleName: "name" },
|
|
{ value: SortOrderComboBox.TokenOrderGroupName, text: qsTr("Collection/community name"), icon: "group", sortRoleName: "groupName" }, // Custom SFPM role communityName || collectionName
|
|
{ value: SortOrderComboBox.TokenOrderCustom, text: qsTr("Custom order"), icon: "exchange", sortRoleName: "" },
|
|
{ value: SortOrderComboBox.TokenOrderNone, text: "---", icon: "", sortRoleName: "" }, // separator
|
|
{ value: SortOrderComboBox.TokenOrderCreateCustom, text: hasCustomOrderDefined ? qsTr("Edit custom order →") : qsTr("Create custom order →"),
|
|
icon: "", sortRoleName: "" }
|
|
]
|
|
onCreateOrEditRequested: {
|
|
root.manageTokensRequested()
|
|
}
|
|
}
|
|
|
|
Item { Layout.fillWidth: true }
|
|
|
|
StatusLinkText {
|
|
visible: cmbFilter.hasEnabledFilters
|
|
normalColor: Theme.palette.primaryColor1
|
|
text: qsTr("Clear filter")
|
|
onClicked: cmbFilter.clearFilter()
|
|
}
|
|
}
|
|
|
|
StatusDialogDivider {
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
ShapeRectangle {
|
|
Layout.fillWidth: true
|
|
Layout.topMargin: Style.current.padding
|
|
visible: !d.hasCollectibles && !d.hasCommunityCollectibles
|
|
text: qsTr("Collectibles will appear here")
|
|
}
|
|
|
|
DoubleFlickableWithFolding {
|
|
id: doubleFlickable
|
|
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
clip: true
|
|
|
|
flickable1: CustomGridView {
|
|
id: communityCollectiblesView
|
|
|
|
header: HeaderDelegate {
|
|
height: d.headerHeight
|
|
width: doubleFlickable.width
|
|
z: 1
|
|
|
|
text: qsTr("Community minted")
|
|
|
|
scrolled: !doubleFlickable.atYBeginning
|
|
checked: doubleFlickable.flickable1Folded
|
|
|
|
onToggleClicked: doubleFlickable.flip1Folding()
|
|
onInfoClicked: Global.openPopup(communityInfoPopupCmp)
|
|
}
|
|
|
|
Binding {
|
|
target: communityCollectiblesView
|
|
property: "header"
|
|
when: d.onlyOneType
|
|
value: null
|
|
|
|
restoreMode: Binding.RestoreBindingOrValue
|
|
}
|
|
|
|
width: doubleFlickable.width
|
|
cellHeight: d.communityCellHeight
|
|
|
|
model: d.communityModelWithLoadingItems
|
|
}
|
|
|
|
flickable2: CustomGridView {
|
|
id: regularCollectiblesView
|
|
|
|
header: HeaderDelegate {
|
|
height: d.headerHeight
|
|
width: doubleFlickable.width
|
|
z: 1
|
|
|
|
text: qsTr("Others")
|
|
|
|
checked: doubleFlickable.flickable2Folded
|
|
scrolled: (doubleFlickable.contentY >
|
|
communityCollectiblesView.contentHeight
|
|
- d.headerHeight)
|
|
showInfoButton: false
|
|
|
|
onToggleClicked: doubleFlickable.flip2Folding()
|
|
}
|
|
|
|
Binding {
|
|
target: regularCollectiblesView
|
|
property: "header"
|
|
when: d.onlyOneType
|
|
value: null
|
|
|
|
restoreMode: Binding.RestoreBindingOrValue
|
|
}
|
|
|
|
width: doubleFlickable.width
|
|
cellHeight: d.cellHeight
|
|
|
|
model: d.nonCommunityModelWithLoadingItems
|
|
}
|
|
}
|
|
|
|
component HeaderDelegate: Rectangle {
|
|
id: sectionDelegate
|
|
|
|
property alias text: headerLabel.text
|
|
property alias checked: toggleButton.checked
|
|
property bool scrolled: false
|
|
property alias showInfoButton: infoButton.visible
|
|
|
|
signal toggleClicked
|
|
signal infoClicked
|
|
|
|
color: Theme.palette.statusListItem.backgroundColor
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
|
|
StatusFlatButton {
|
|
id: toggleButton
|
|
|
|
checkable: true
|
|
size: StatusBaseButton.Size.Small
|
|
icon.name: checked ? "next" : "chevron-down"
|
|
textColor: Theme.palette.baseColor1
|
|
textHoverColor: Theme.palette.directColor1
|
|
|
|
onToggled: sectionDelegate.toggleClicked()
|
|
}
|
|
StatusBaseText {
|
|
id: headerLabel
|
|
|
|
Layout.fillWidth: true
|
|
|
|
color: Theme.palette.baseColor1
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
StatusFlatButton {
|
|
id: infoButton
|
|
|
|
icon.name: "info"
|
|
textColor: Theme.palette.baseColor1
|
|
|
|
onClicked: sectionDelegate.infoClicked()
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width
|
|
height: 4
|
|
anchors.top: parent.bottom
|
|
|
|
color: Theme.palette.directColor8
|
|
visible: !sectionDelegate.checked && sectionDelegate.scrolled
|
|
}
|
|
}
|
|
|
|
component CustomGridView: StatusGridView {
|
|
id: gridView
|
|
|
|
interactive: false
|
|
|
|
cellWidth: d.cellWidth
|
|
delegate: collectibleDelegate
|
|
}
|
|
|
|
Component {
|
|
id: collectibleDelegate
|
|
CollectibleView {
|
|
width: d.cellWidth
|
|
height: isCommunityCollectible ? d.communityCellHeight : d.cellHeight
|
|
title: model.name ? model.name : "..."
|
|
subTitle: model.collectionName ? model.collectionName : model.collectionUid ? model.collectionUid : ""
|
|
mediaUrl: model.mediaUrl ?? ""
|
|
mediaType: model.mediaType ?? ""
|
|
fallbackImageUrl: model.imageUrl ?? ""
|
|
backgroundColor: model.backgroundColor ? model.backgroundColor : "transparent"
|
|
isLoading: !!model.isLoading
|
|
privilegesLevel: model.communityPrivilegesLevel ?? Constants.TokenPrivilegesLevel.Community
|
|
ornamentColor: model.communityColor ?? "transparent"
|
|
communityId: model.communityId ?? ""
|
|
communityName: model.communityName ?? ""
|
|
communityImage: model.communityImage ?? ""
|
|
balance: model.balance ?? 1
|
|
|
|
onClicked: root.collectibleClicked(model.chainId, model.contractAddress, model.tokenId, model.symbol, model.tokenType)
|
|
onRightClicked: {
|
|
Global.openMenu(tokenContextMenu, this,
|
|
{symbol: model.symbol, tokenName: model.name, tokenImage: model.imageUrl,
|
|
communityId: model.communityId, communityName: model.communityName,
|
|
communityImage: model.communityImage, tokenType: model.tokenType})
|
|
}
|
|
onSwitchToCommunityRequested: (communityId) => root.switchToCommunityRequested(communityId)
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: tokenContextMenu
|
|
StatusMenu {
|
|
onClosed: destroy()
|
|
|
|
property string symbol
|
|
property string tokenName
|
|
property string tokenImage
|
|
property string communityId
|
|
property string communityName
|
|
property string communityImage
|
|
property int tokenType
|
|
|
|
StatusAction {
|
|
enabled: root.sendEnabled
|
|
visibleOnDisabled: true
|
|
icon.name: "send"
|
|
text: qsTr("Send")
|
|
onTriggered: root.sendRequested(symbol, tokenType)
|
|
}
|
|
StatusAction {
|
|
icon.name: "receive"
|
|
text: qsTr("Receive")
|
|
onTriggered: root.receiveRequested(symbol)
|
|
}
|
|
StatusMenuSeparator {}
|
|
StatusAction {
|
|
icon.name: "settings"
|
|
text: qsTr("Manage tokens")
|
|
onTriggered: root.manageTokensRequested()
|
|
}
|
|
StatusAction {
|
|
enabled: symbol !== Constants.ethToken
|
|
type: StatusAction.Type.Danger
|
|
icon.name: "hide"
|
|
text: qsTr("Hide collectible")
|
|
onTriggered: Global.openConfirmHideCollectiblePopup(symbol, tokenName, tokenImage, !!communityId)
|
|
}
|
|
StatusAction {
|
|
enabled: !!communityId
|
|
type: StatusAction.Type.Danger
|
|
icon.name: "hide"
|
|
text: qsTr("Hide all collectibles from this community")
|
|
onTriggered: Global.openPopup(confirmHideCommunityCollectiblesPopup, {communityId, communityName, communityImage})
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: communityInfoPopupCmp
|
|
StatusDialog {
|
|
destroyOnClose: true
|
|
title: qsTr("What are community collectibles?")
|
|
standardButtons: Dialog.Ok
|
|
width: 520
|
|
contentItem: StatusBaseText {
|
|
wrapMode: Text.Wrap
|
|
text: qsTr("Community collectibles are collectibles that have been minted by a community. As these collectibles cannot be verified, always double check their origin and validity before interacting with them. If in doubt, ask a trusted member or admin of the relevant community.")
|
|
}
|
|
}
|
|
}
|
|
|
|
Component {
|
|
id: confirmHideCommunityCollectiblesPopup
|
|
ConfirmationDialog {
|
|
property string communityId
|
|
property string communityName
|
|
property string communityImage
|
|
|
|
width: 520
|
|
destroyOnClose: true
|
|
confirmButtonLabel: qsTr("Hide '%1' collectibles").arg(communityName)
|
|
cancelBtnType: ""
|
|
showCancelButton: true
|
|
headerSettings.title: qsTr("Hide %1 community collectibles").arg(communityName)
|
|
headerSettings.asset.name: communityImage
|
|
confirmationText: qsTr("Are you sure you want to hide all community collectibles minted by %1? You will no longer see or be able to interact with these collectibles anywhere inside Status.").arg(communityName)
|
|
onCancelButtonClicked: close()
|
|
onConfirmButtonClicked: {
|
|
root.controller.showHideGroup(communityId, false)
|
|
close()
|
|
Global.displayToastMessage(
|
|
qsTr("%1 community collectibles were successfully hidden. You can toggle collectible visibility via %2.").arg(communityName)
|
|
.arg(`<a style="text-decoration:none" href="#${Constants.appSection.profile}/${Constants.settingsSubsection.wallet}/${Constants.walletSettingsSubsection.manageCollectibles}">` + qsTr("Settings", "Go to Settings") + "</a>"),
|
|
"",
|
|
"checkmark-circle",
|
|
false,
|
|
Constants.ephemeralNotificationType.success,
|
|
""
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|