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.Internal 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 collectiblesModel required property string addressFilters required property string networkFilters property bool sendEnabled: true property bool filterVisible signal collectibleClicked(int chainId, string contractAddress, string tokenId, string uid) signal sendRequested(string symbol) 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 renamedModel: RolesRenamingModel { sourceModel: root.collectiblesModel mapping: [ RoleRename { from: "uid" to: "symbol" } ] } readonly property bool hasCollectibles: nonCommunityModel.count readonly property bool hasCommunityCollectibles: communityModel.count readonly property bool onlyOneType: !hasCollectibles || !hasCommunityCollectibles readonly property var controller: ManageTokensController { settingsKey: "WalletCollectibles" sourceModel: d.renamedModel } readonly property var nwFilters: root.networkFilters.split(":") readonly property var addrFilters: root.addressFilters.split(":").map((addr) => addr.toLowerCase()) function hideAllCommunityTokens(communityId) { const tokenSymbols = ModelUtils.getAll(communityCollectiblesView.model, "symbol", "communityId", communityId) d.controller.settingsHideGroupTokens(communityId, tokenSymbols) } function containsAny(list, filterList) { for (let i = 0; i < list.length; i++) { if (filterList.includes(list[i].toLowerCase())) { return true } } return false } } component CustomSFPM: SortFilterProxyModel { id: customFilter property bool isCommunity sourceModel: d.renamedModel proxyRoles: JoinRole { name: "groupName" roleNames: ["collectionName", "communityName"] } filters: [ FastExpressionFilter { expression: { d.addrFilters return d.nwFilters.includes(model.chainId+"") && d.containsAny(model.ownershipAddresses.split(":"), d.addrFilters) } expectedRoles: ["chainId", "ownershipAddresses"] }, FastExpressionFilter { expression: { d.controller.settingsDirty return d.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: { d.controller.settingsDirty return d.controller.compareTokens(modelLeft.symbol, modelRight.symbol) } enabled: d.isCustomView expectedRoles: ["symbol"] }, RoleSorter { roleName: cmbTokenOrder.currentSortRoleName sortOrder: cmbTokenOrder.currentSortOrder enabled: !d.isCustomView } ] } CustomSFPM { id: communityModel isCommunity: true } CustomSFPM { id: nonCommunityModel } 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: d.controller.regularTokensModel collectionGroupsModel: d.controller.collectionGroupsModel communityTokenGroupsModel: d.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: d.controller.hasSettings model: [ { value: SortOrderComboBox.TokenOrderDateAdded, text: qsTr("Date added"), icon: "calendar", sortRoleName: "dateAdded" }, // FIXME sortRoleName #12942 { 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: communityModel } 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: nonCommunityModel } } 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 ? "chevron-down" : "next" 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 ?? "" onClicked: root.collectibleClicked(model.chainId, model.contractAddress, model.tokenId, model.symbol) onRightClicked: { Global.openMenu(tokenContextMenu, this, {symbol: model.symbol, tokenName: model.name, tokenImage: model.imageUrl, communityId: model.communityId, communityName: model.communityName, communityImage: model.communityImage}) } 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 StatusAction { enabled: root.sendEnabled icon.name: "send" text: qsTr("Send") onTriggered: root.sendRequested(symbol) } 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 !== "ETH" type: StatusAction.Type.Danger icon.name: "hide" text: qsTr("Hide collectible") onTriggered: Global.openPopup(confirmHideCollectiblePopup, {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: confirmHideCollectiblePopup ConfirmationDialog { property string symbol property string tokenName property string tokenImage property string communityId readonly property string formattedName: tokenName + (communityId ? " (" + qsTr("community collectible") + ")" : "") width: 520 destroyOnClose: true confirmButtonLabel: qsTr("Hide %1").arg(tokenName) cancelBtnType: "" showCancelButton: true headerSettings.title: qsTr("Hide %1").arg(formattedName) headerSettings.asset.name: tokenImage confirmationText: qsTr("Are you sure you want to hide %1? You will no longer see or be able to interact with this collectible anywhere inside Status.").arg(formattedName) onCancelButtonClicked: close() onConfirmButtonClicked: { d.controller.settingsHideToken(symbol) close() Global.displayToastMessage( qsTr("%1 was successfully hidden. You can toggle collectible visibility via %2.").arg(formattedName) .arg(`` + qsTr("Settings", "Go to Settings") + ""), "", "checkmark-circle", false, Constants.ephemeralNotificationType.success, "" ) } } } Component { id: confirmHideCommunityCollectiblesPopup ConfirmationDialog { property string communityId property string communityName property string communityImage width: 520 destroyOnClose: true confirmButtonLabel: qsTr("Hide all collectibles minted by this community") 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: { d.hideAllCommunityTokens(communityId) close() Global.displayToastMessage( qsTr("%1 community collectibles were successfully hidden. You can toggle collectible visibility via %2.").arg(communityName) .arg(`` + qsTr("Settings", "Go to Settings") + ""), "", "checkmark-circle", false, Constants.ephemeralNotificationType.success, "" ) } } } }