diff --git a/Explorer/Assets/DCL/AvatarRendering/Emotes/Tests/FinalizeEmoteLoadingSystemShould.cs b/Explorer/Assets/DCL/AvatarRendering/Emotes/Tests/FinalizeEmoteLoadingSystemShould.cs index 8d8c4b0d03..d2d4391765 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Emotes/Tests/FinalizeEmoteLoadingSystemShould.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Emotes/Tests/FinalizeEmoteLoadingSystemShould.cs @@ -575,6 +575,11 @@ public void ClearOwnedNftRegistry() throw new NotImplementedException(); } + public void ClearOwnedNftForUrn(URN nftUrn) + { + throw new NotImplementedException(); + } + public bool TryGetLatestTransferredAt(URN nftUrn, out DateTime latestTransferredAt) { throw new NotImplementedException(); diff --git a/Explorer/Assets/DCL/AvatarRendering/Loading/IAvatarElementStorage.cs b/Explorer/Assets/DCL/AvatarRendering/Loading/IAvatarElementStorage.cs index c546fbf6fc..91246fcd2e 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Loading/IAvatarElementStorage.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Loading/IAvatarElementStorage.cs @@ -44,6 +44,12 @@ public interface IAvatarElementStorage where TElement: IAvata int GetOwnedNftCount(URN nftUrn); void ClearOwnedNftRegistry(); + + /// + /// Clears the owned NFT entries for a specific base URN. + /// Call this before repopulating with fresh data from the API. + /// + void ClearOwnedNftForUrn(URN nftUrn); } public static class AvatarElementCache diff --git a/Explorer/Assets/DCL/AvatarRendering/Loading/Systems/LoadElementsByIntentionSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Loading/Systems/LoadElementsByIntentionSystem.cs index c4ebb8d919..a021cca587 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Loading/Systems/LoadElementsByIntentionSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Loading/Systems/LoadElementsByIntentionSystem.cs @@ -115,6 +115,7 @@ private async UniTask ProcessElementAsync(ILambdaResponseElement await AssetBundleManifestFallbackHelper.CheckAssetBundleManifestFallbackAsync(World, avatarElement.DTO, partition, ct); // Process individual data (this part needs to remain sequential per element for thread safety) + // Note: We use API's amount directly for display, registry is only for token ID tracking foreach (ElementIndividualDataDto individualData in element.IndividualData) { // Probably a base wearable, wrongly return individual data. Skip it diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/DTO/TrimmedWearableDTO.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/DTO/TrimmedWearableDTO.cs index 0a01119186..cde00ab79c 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/DTO/TrimmedWearableDTO.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Helpers/DTO/TrimmedWearableDTO.cs @@ -40,12 +40,16 @@ public struct LambdaResponse : IAttachmentLambdaResponse { public TrimmedWearableDTO entity; + public int amount; [JsonIgnore] public TrimmedWearableDTO Entity => entity; [JsonIgnore] public IReadOnlyList IndividualData => entity.individualData; + + [JsonIgnore] + public int Amount => amount; } } diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Registry/AvatarElementNftRegistry.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Registry/AvatarElementNftRegistry.cs index 9f17026f41..0261185e52 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Registry/AvatarElementNftRegistry.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Registry/AvatarElementNftRegistry.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using CommunicationData.URLHelpers; using DCL.AvatarRendering.Wearables.Components; @@ -50,6 +50,21 @@ public void ClearOwnedNftRegistry() } } + /// + /// Clears the owned NFT entries for a specific base URN. + /// Call this before repopulating with fresh data from the API. + /// + public void ClearOwnedNftForUrn(URN nftUrn) + { + lock (lockObject) + { + if (ownedNftRegistry.TryGetValue(nftUrn, out var registry)) + { + registry.Clear(); + } + } + } + public bool TryGetLatestTransferredAt(URN nftUrn, out DateTime latestTransferredAt) { lock (lockObject) diff --git a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/Load/LoadWearablesByParamSystem.cs b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/Load/LoadWearablesByParamSystem.cs index 24334630ca..ad1fef6cba 100644 --- a/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/Load/LoadWearablesByParamSystem.cs +++ b/Explorer/Assets/DCL/AvatarRendering/Wearables/Systems/Load/LoadWearablesByParamSystem.cs @@ -93,6 +93,8 @@ protected sealed override async UniTask ProcessElementAsync(ILambdaResponseEleme else await AssetBundleManifestFallbackHelper.CheckAssetBundleManifestFallbackAsync(World, wearable.TrimmedDTO, partition, ct); + // Get amount directly from API response if available (more reliable than registry count) + int apiAmount = 0; + if (element is TrimmedWearableDTO.LambdaResponseElementDto trimmedElement) + apiAmount = trimmedElement.Amount; + + ReportHub.Log(ReportCategory.GIFTING, $"[LoadWearablesByParamSystem] Processing: {elementDTO.Metadata.id}, API Amount: {apiAmount}, IndividualData count: {element.IndividualData?.Count ?? 0}"); + if (element.IndividualData != null) + { // Process individual data (this part needs to remain sequential per element for thread safety) + // Note: We use API's amount directly for display, registry is only for token ID tracking foreach (var individualData in element.IndividualData) { // Probably a base wearable, wrongly return individual data. Skip it @@ -194,8 +207,11 @@ private async UniTask ProcessElementAsync(ILambdaResponseEleme ReportHub.Log(ReportCategory.OUTFITS, $"[WEARABLE_STORAGE_POPULATED] Key: '{elementDTO.Metadata.id}' now maps to Value: '{individualData.id}' (Token: {individualData.tokenId})"); } + } - int ownedAmount = avatarElementStorage.GetOwnedNftCount(elementDTO.Metadata.id); + // Use API amount directly - it's the source of truth from the indexer + // Fall back to registry count only if API amount not available + int ownedAmount = apiAmount > 0 ? apiAmount : avatarElementStorage.GetOwnedNftCount(elementDTO.Metadata.id); wearable.SetAmount(ownedAmount); return wearable; } diff --git a/Explorer/Assets/DCL/Backpack/Assets/BackpackEmoteGridItem.prefab b/Explorer/Assets/DCL/Backpack/Assets/BackpackEmoteGridItem.prefab index 810c5f2814..86505f94f4 100644 --- a/Explorer/Assets/DCL/Backpack/Assets/BackpackEmoteGridItem.prefab +++ b/Explorer/Assets/DCL/Backpack/Assets/BackpackEmoteGridItem.prefab @@ -103,6 +103,7 @@ MonoBehaviour: m_VerticalAlignment: 512 m_textAlignment: 65535 m_characterSpacing: 0 + m_characterHorizontalScale: 1 m_wordSpacing: 0 m_lineSpacing: 0 m_lineSpacingMax: 0 @@ -176,6 +177,26 @@ PrefabInstance: propertyPath: m_IsActive value: 0 objectReference: {fileID: 0} + - target: {fileID: 3950903425550913156, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3950903425550913156, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3950903425550913156, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3950903425550913156, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 3950903425550913156, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} - target: {fileID: 4450241265784993082, guid: aabac5666448b4345a76347f94ea32a4, type: 3} propertyPath: m_PixelsPerUnitMultiplier value: 2 @@ -288,6 +309,10 @@ PrefabInstance: propertyPath: m_Name value: BackpackEmoteGridItem objectReference: {fileID: 0} + - target: {fileID: 6687238669057343753, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} - target: {fileID: 8326074706031102082, guid: aabac5666448b4345a76347f94ea32a4, type: 3} propertyPath: m_AnchoredPosition.y value: 6.5 @@ -344,6 +369,11 @@ PrefabInstance: insertIndex: -1 addedObject: {fileID: 7445961164079291941} m_SourcePrefab: {fileID: 100100000, guid: aabac5666448b4345a76347f94ea32a4, type: 3} +--- !u!1 &122965643622874184 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 2117753677444134449, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + m_PrefabInstance: {fileID: 2078157404631091833} + m_PrefabAsset: {fileID: 0} --- !u!114 &719150229022247077 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 1526146829364839132, guid: aabac5666448b4345a76347f94ea32a4, type: 3} @@ -392,6 +422,11 @@ GameObject: m_CorrespondingSourceObject: {fileID: 3604137211807971006, guid: aabac5666448b4345a76347f94ea32a4, type: 3} m_PrefabInstance: {fileID: 2078157404631091833} m_PrefabAsset: {fileID: 0} +--- !u!1 &3839358371985702382 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 2999183216935663511, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + m_PrefabInstance: {fileID: 2078157404631091833} + m_PrefabAsset: {fileID: 0} --- !u!1 &4228669688390808703 stripped GameObject: m_CorrespondingSourceObject: {fileID: 2772064026238959110, guid: aabac5666448b4345a76347f94ea32a4, type: 3} @@ -408,6 +443,17 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} m_Name: m_EditorClassIdentifier: +--- !u!114 &4890261822838372131 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 6848473177564986714, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + m_PrefabInstance: {fileID: 2078157404631091833} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI --- !u!1 &5091980877140501665 stripped GameObject: m_CorrespondingSourceObject: {fileID: 6520438785063841496, guid: aabac5666448b4345a76347f94ea32a4, type: 3} @@ -429,6 +475,8 @@ MonoBehaviour: k__BackingField: {fileID: 5337782633102568864} k__BackingField: {fileID: 6749943394813585919} k__BackingField: {fileID: 5665178272263481625} + k__BackingField: {fileID: 0} + k__BackingField: {fileID: 0} k__BackingField: {fileID: 7903757533396073920} k__BackingField: {fileID: 1877216464682181992} k__BackingField: {fileID: 1516561254018689797} @@ -441,6 +489,10 @@ MonoBehaviour: k__BackingField: 0 incompatibleWithBodyShapeContainer: {fileID: 3374162131891453127} incompatibleWithBodyShapeHoverContainer: {fileID: 4228669688390808703} + pendingTransferContainer: {fileID: 3839358371985702382} + pendingTransferHoverContainer: {fileID: 8045011785916068164} + nftCountContainer: {fileID: 122965643622874184} + nftCountText: {fileID: 4890261822838372131} k__BackingField: {fileID: 0} k__BackingField: {fileID: 11400000, guid: d1d7149ae3c90e449b657edfd7109eff, type: 2} k__BackingField: {fileID: 11400000, guid: b78ff33cb1d284d4b8938bf362676581, type: 2} @@ -500,3 +552,8 @@ GameObject: m_CorrespondingSourceObject: {fileID: 8241648339715965561, guid: aabac5666448b4345a76347f94ea32a4, type: 3} m_PrefabInstance: {fileID: 2078157404631091833} m_PrefabAsset: {fileID: 0} +--- !u!1 &8045011785916068164 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 8318859614241815357, guid: aabac5666448b4345a76347f94ea32a4, type: 3} + m_PrefabInstance: {fileID: 2078157404631091833} + m_PrefabAsset: {fileID: 0} diff --git a/Explorer/Assets/DCL/Backpack/Assets/BackpackItem.prefab b/Explorer/Assets/DCL/Backpack/Assets/BackpackItem.prefab index c8e06981e1..ab06c9b607 100644 --- a/Explorer/Assets/DCL/Backpack/Assets/BackpackItem.prefab +++ b/Explorer/Assets/DCL/Backpack/Assets/BackpackItem.prefab @@ -545,6 +545,124 @@ MonoBehaviour: m_EditorClassIdentifier: m_HorizontalFit: 0 m_VerticalFit: 2 +--- !u!1 &2117753677444134449 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6687238669057343753} + - component: {fileID: 1595998469460224237} + - component: {fileID: 6341271998918608969} + - component: {fileID: 3471428184783491682} + - component: {fileID: 8384502883240172721} + m_Layer: 5 + m_Name: NftCount + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6687238669057343753 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2117753677444134449} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 3950903425550913156} + m_Father: {fileID: 9217950789451999232} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 1, y: 0} + m_AnchorMax: {x: 1, y: 0} + m_AnchoredPosition: {x: -6, y: 6} + m_SizeDelta: {x: 0, y: 18} + m_Pivot: {x: 1, y: 0} +--- !u!222 &1595998469460224237 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2117753677444134449} + m_CullTransparentMesh: 1 +--- !u!114 &6341271998918608969 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2117753677444134449} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 0.26666668, g: 0.24313727, b: 0.30980393, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: f8363e0e2ac6a4edd846de9a5f322e9a, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 2 +--- !u!114 &3471428184783491682 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2117753677444134449} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30649d3a9faa99c48a7b1166b86bf2a0, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.HorizontalLayoutGroup + m_Padding: + m_Left: 4 + m_Right: 4 + m_Top: 0 + m_Bottom: 0 + m_ChildAlignment: 4 + m_Spacing: 0 + m_ChildForceExpandWidth: 0 + m_ChildForceExpandHeight: 0 + m_ChildControlWidth: 1 + m_ChildControlHeight: 0 + m_ChildScaleWidth: 0 + m_ChildScaleHeight: 0 + m_ReverseArrangement: 0 +--- !u!114 &8384502883240172721 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2117753677444134449} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter + m_HorizontalFit: 2 + m_VerticalFit: 0 --- !u!1 &2772064026238959110 GameObject: m_ObjectHideFlags: 0 @@ -1057,6 +1175,81 @@ MonoBehaviour: m_OnClick: m_PersistentCalls: m_Calls: [] +--- !u!1 &2999183216935663511 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 512186170824626682} + - component: {fileID: 177945317462343749} + - component: {fileID: 1602375715386022469} + m_Layer: 5 + m_Name: Overlay + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &512186170824626682 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2999183216935663511} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 6431537697271910309} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0.75} + m_SizeDelta: {x: 0, y: -1.5} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &177945317462343749 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2999183216935663511} + m_CullTransparentMesh: 1 +--- !u!114 &1602375715386022469 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2999183216935663511} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.08627451, g: 0.08235294, b: 0.09411765, a: 0.8} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 12dd1efc4e826764f9b02be515a9a033, type: 3} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 2.2 --- !u!1 &3245215369780417134 GameObject: m_ObjectHideFlags: 0 @@ -1478,6 +1671,143 @@ RectTransform: m_AnchoredPosition: {x: -8, y: -8} m_SizeDelta: {x: 64, y: 18} m_Pivot: {x: 1, y: 1} +--- !u!1 &5968308742855456404 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3950903425550913156} + - component: {fileID: 6540220418989932363} + - component: {fileID: 6848473177564986714} + m_Layer: 5 + m_Name: Label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3950903425550913156 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5968308742855456404} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 6687238669057343753} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 14.36} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &6540220418989932363 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5968308742855456404} + m_CullTransparentMesh: 1 +--- !u!114 &6848473177564986714 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5968308742855456404} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: x2 + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2} + m_sharedMaterial: {fileID: 735423033564544980, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 12 + m_fontSizeBase: 12 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 1 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 0 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} --- !u!1 &6520438785063841496 GameObject: m_ObjectHideFlags: 0 @@ -1546,6 +1876,10 @@ MonoBehaviour: k__BackingField: 0 incompatibleWithBodyShapeContainer: {fileID: 3604137211807971006} incompatibleWithBodyShapeHoverContainer: {fileID: 2772064026238959110} + pendingTransferContainer: {fileID: 2999183216935663511} + pendingTransferHoverContainer: {fileID: 8318859614241815357} + nftCountContainer: {fileID: 2117753677444134449} + nftCountText: {fileID: 6848473177564986714} k__BackingField: {fileID: 4819163676585692057} k__BackingField: {fileID: 11400000, guid: d1d7149ae3c90e449b657edfd7109eff, type: 2} k__BackingField: {fileID: 11400000, guid: b78ff33cb1d284d4b8938bf362676581, type: 2} @@ -1756,6 +2090,8 @@ RectTransform: - {fileID: 950865159361347939} - {fileID: 8326074706031102082} - {fileID: 8778587071951130392} + - {fileID: 6431537697271910309} + - {fileID: 6687238669057343753} - {fileID: 7082751518765691579} - {fileID: 5208444472304398528} m_Father: {fileID: 6252267455741274073} @@ -1765,6 +2101,221 @@ RectTransform: m_AnchoredPosition: {x: 0, y: -69.08783} m_SizeDelta: {x: 126, y: 126.1757} m_Pivot: {x: 0.5, y: 0.5} +--- !u!1 &8318859614241815357 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 266988240685495202} + - component: {fileID: 748687988300397984} + - component: {fileID: 1292353412624770024} + - component: {fileID: 8631761791243242745} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &266988240685495202 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8318859614241815357} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 6431537697271910309} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -20, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &748687988300397984 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8318859614241815357} + m_CullTransparentMesh: 1 +--- !u!114 &1292353412624770024 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8318859614241815357} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: New Text + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2} + m_sharedMaterial: {fileID: 735423033564544980, guid: 96ae0a2159a39234f858ea23bdcc74ad, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 13 + m_fontSizeBase: 13 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_characterHorizontalScale: 1 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!114 &8631761791243242745 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8318859614241815357} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 56eb0353ae6e5124bb35b17aff880f16, type: 3} + m_Name: + m_EditorClassIdentifier: + m_StringReference: + m_TableReference: + m_TableCollectionName: GUID:fa6d78a0e08f14179b202ccca9720bb9 + m_TableEntryReference: + m_KeyId: 299644359562919936 + m_Key: + m_FallbackState: 0 + m_WaitForCompletion: 0 + m_LocalVariables: [] + m_FormatArguments: [] + m_UpdateString: + m_PersistentCalls: + m_Calls: + - m_Target: {fileID: 1292353412624770024} + m_TargetAssemblyTypeName: TMPro.TMP_Text, Unity.TextMeshPro + m_MethodName: set_text + m_Mode: 0 + m_Arguments: + m_ObjectArgument: {fileID: 0} + m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine + m_IntArgument: 0 + m_FloatArgument: 0 + m_StringArgument: + m_BoolArgument: 0 + m_CallState: 1 + references: + version: 2 + RefIds: [] +--- !u!1 &8880512928635897717 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 6431537697271910309} + m_Layer: 5 + m_Name: PendingTransferContainer + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &6431537697271910309 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8880512928635897717} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 512186170824626682} + - {fileID: 266988240685495202} + m_Father: {fileID: 9217950789451999232} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} --- !u!1001 &1779941017573495427 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Explorer/Assets/DCL/Backpack/AvatarSection/Outfits/OutfitsPresenter.cs b/Explorer/Assets/DCL/Backpack/AvatarSection/Outfits/OutfitsPresenter.cs index 3fd7f442c9..04653e8e2a 100644 --- a/Explorer/Assets/DCL/Backpack/AvatarSection/Outfits/OutfitsPresenter.cs +++ b/Explorer/Assets/DCL/Backpack/AvatarSection/Outfits/OutfitsPresenter.cs @@ -5,6 +5,7 @@ using CommunicationData.URLHelpers; using Cysharp.Threading.Tasks; using DCL.AvatarRendering.Wearables.Equipped; +using DCL.AvatarRendering.Wearables.Helpers; using DCL.Backpack.AvatarSection.Outfits; using DCL.Backpack.AvatarSection.Outfits.Banner; using DCL.Backpack.AvatarSection.Outfits.Commands; @@ -13,6 +14,8 @@ using DCL.Backpack.AvatarSection.Outfits.Services; using DCL.Backpack.AvatarSection.Outfits.Slots; using DCL.Backpack.CharacterPreview; +using DCL.Backpack.Gifting.Services.PendingTransfers; +using DCL.Backpack.Gifting.Utils; using DCL.Backpack.Slots; using DCL.Browser; using DCL.CharacterPreview; @@ -43,7 +46,8 @@ public class OutfitsPresenter : ISection, IDisposable private readonly CheckOutfitsBannerVisibilityCommand bannerVisibilityCommand; private readonly PrewarmWearablesCacheCommand prewarmWearablesCacheCommand; private readonly PreviewOutfitCommand previewOutfitCommand; - + private readonly IWearableStorage wearableStorage; + private readonly IPendingTransferService pendingTransferService; private readonly List slotPresenters = new (); private CancellationTokenSource cts = new (); @@ -61,7 +65,9 @@ public OutfitsPresenter(OutfitsView view, PreviewOutfitCommand previewOutfitCommand, IAvatarScreenshotService screenshotService, CharacterPreviewControllerBase characterPreviewController, - OutfitSlotPresenterFactory slotFactory) + OutfitSlotPresenterFactory slotFactory, + IWearableStorage wearableStorage, + IPendingTransferService pendingTransferService) { this.view = view; this.eventBus = eventBus; @@ -78,6 +84,8 @@ public OutfitsPresenter(OutfitsView view, this.screenshotService = screenshotService; this.characterPreviewController = characterPreviewController; this.slotFactory = slotFactory; + this.wearableStorage = wearableStorage; + this.pendingTransferService = pendingTransferService; outfitBannerPresenter = new OutfitBannerPresenter(view.OutfitsBanner, OnGetANameClicked, OnLinkClicked); @@ -126,14 +134,20 @@ private async UniTask RefreshOutfitsAsync(CancellationToken ct) { SetAllSlotsToLoading(); + // Load raw outfits var outfits = await LoadAndCacheOutfitsAsync(ct); if (ct.IsCancellationRequested) return; // Precache wearables in the background. PrewarmWearablesCacheAsync(outfits.Values, ct).Forget(); + // Filter Blocked Outfits + // We create a new dictionary containing ONLY the outfits that are safe to wear. + // The ones with pending transfers will be excluded, making the slot appear Empty. + var validOutfits = FilterBlockedOutfits(outfits); + // Update the UI with the primary data. - PopulateAllSlots(outfits); + PopulateAllSlots(validOutfits); } catch (OperationCanceledException) { @@ -176,6 +190,73 @@ private async UniTaskVoid PrewarmWearablesCacheAsync(IEnumerable out await prewarmWearablesCacheCommand.ExecuteAsync(uniqueUrnsToPrewarm, ct); } + private Dictionary FilterBlockedOutfits(IReadOnlyDictionary source) + { + var result = new Dictionary(); + + foreach (var kvp in source) + { + // If returns TRUE (it is blocked), we skip adding it. + // The slot will render as Empty. + if (IsOutfitBlocked(kvp.Value.outfit)) + continue; + + result.Add(kvp.Key, kvp.Value); + } + + return result; + } + + private bool IsOutfitBlocked(Outfit? outfit) + { + if (outfit == null || outfit.wearables == null) return false; + + // Check BodyShape + if (!string.IsNullOrEmpty(outfit.bodyShape) && IsItemBlocked(outfit.bodyShape)) + return true; + + // Check all Wearables + foreach (string urn in outfit.wearables) + { + if (IsItemBlocked(urn)) + return true; + } + + return false; + } + + private bool IsItemBlocked(string urn) + { + // Convert string to URN object to access helper methods + URN urnObj = new URN(urn); + + // Off-chain (Base) items are never blocked by pending transfers. + // They don't live in the NFT registry, so we skip the count check for them. + if (urnObj.IsBaseWearable()) + return false; + + + // Check availability based on Pending Transfers + if (!GiftingUrnParsingHelper.TryGetBaseUrn(urn, out string baseUrn)) + baseUrn = urn; + + URN baseUrnObj = new URN(baseUrn); + + // Get total owned in inventory + int totalOwned = 0; + if (wearableStorage.TryGetOwnedNftRegistry(baseUrnObj, out var registry)) + totalOwned = registry.Count; + + // Get pending count + int pendingCount = pendingTransferService.GetPendingCount(baseUrn); + + // Calculate available amount + int available = totalOwned - pendingCount; + + // If available is <= 0, this item cannot be equipped, so the outfit is blocked. + return available <= 0; + } + private void OnSaveOutfitRequested(int slotIndex) { OnSaveOutfitRequestedAsync(slotIndex, cts.Token).Forget(); diff --git a/Explorer/Assets/DCL/Backpack/BackpackController.cs b/Explorer/Assets/DCL/Backpack/BackpackController.cs index 0437d32a48..f961627352 100644 --- a/Explorer/Assets/DCL/Backpack/BackpackController.cs +++ b/Explorer/Assets/DCL/Backpack/BackpackController.cs @@ -30,6 +30,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using DCL.Backpack.Gifting.Services.PendingTransfers; using UnityEngine; using Utility; using Avatar = DCL.Profiles.Avatar; @@ -92,7 +93,9 @@ public BackpackController( INftNamesProvider nftNamesProvider, IEventBus eventBus, Sprite deleteIcon, - IDecentralandUrlsSource decentralandUrlsSource) + IDecentralandUrlsSource decentralandUrlsSource, + IPendingTransferService pendingTransferService) + { this.view = view; this.backpackCommandBus = backpackCommandBus; @@ -149,7 +152,9 @@ public BackpackController( previewOutfitCommand, screenshotService, backpackCharacterPreviewController, - outfitSlotFactory); + outfitSlotFactory, + wearableStorage, + pendingTransferService); avatarController = new AvatarController( avatarView, diff --git a/Explorer/Assets/DCL/Backpack/BackpackGridController.cs b/Explorer/Assets/DCL/Backpack/BackpackGridController.cs index 9b08862652..9b163a6cef 100644 --- a/Explorer/Assets/DCL/Backpack/BackpackGridController.cs +++ b/Explorer/Assets/DCL/Backpack/BackpackGridController.cs @@ -1,12 +1,15 @@ using CommunicationData.URLHelpers; using Cysharp.Threading.Tasks; using DCL.AssetsProvision; +using DCL.AvatarRendering.Emotes; +using DCL.AvatarRendering.Loading.Components; using DCL.AvatarRendering.Wearables; using DCL.AvatarRendering.Wearables.Components; using DCL.AvatarRendering.Wearables.Equipped; using DCL.AvatarRendering.Wearables.Helpers; using DCL.Backpack.BackpackBus; using DCL.Backpack.Breadcrumb; +using DCL.Backpack.Gifting.Services.PendingTransfers; using DCL.Browser; using DCL.CharacterPreview; using DCL.Diagnostics; @@ -44,6 +47,8 @@ public class BackpackGridController : IDisposable private readonly IWearableStorage wearableStorage; private readonly SmartWearableCache smartWearableCache; private readonly IMVCManager mvcManager; + private readonly IPendingTransferService pendingTransferService; + private readonly IEmoteStorage emoteStorage; private readonly PageSelectorController pageSelectorController; private readonly BackpackBreadCrumbController breadcrumbController; @@ -79,7 +84,9 @@ public BackpackGridController(BackpackGridView view, ColorPresetsSO bodyshapeColors, IWearableStorage wearableStorage, SmartWearableCache smartWearableCache, - IMVCManager mvcManager) + IMVCManager mvcManager, + IPendingTransferService pendingTransferService, + IEmoteStorage emoteStorage) { this.view = view; this.commandBus = commandBus; @@ -96,6 +103,8 @@ public BackpackGridController(BackpackGridView view, this.smartWearableCache = smartWearableCache; this.mvcManager = mvcManager; this.wearableStorage = wearableStorage; + this.pendingTransferService = pendingTransferService; + this.emoteStorage = emoteStorage; pageSelectorController = new PageSelectorController(view.PageSelectorView, pageButtonView); pageSelectorController.OnSetPage += (int page) => RequestPage(page, false); @@ -184,6 +193,7 @@ private void OnEquipOutfit(BackpackEquipOutfitCommand command, IWearable[] weara } } + itemView.EquippedIcon.SetActive(isEquipped); itemView.IsEquipped = isEquipped; itemView.SetEquipButtonsState(); } @@ -261,8 +271,15 @@ private void SetGridElements(IReadOnlyList gridWearables) && gridWearables[i].GetCategory() != WearableCategories.Categories.EYES && gridWearables[i].GetCategory() != WearableCategories.Categories.EYEBROWS && gridWearables[i].GetCategory() != WearableCategories.Categories.MOUTH; + + URN urn = wearable.GetUrn(); + bool isBaseWearable = urn.IsBaseWearable(); + int totalOwned = wearable.Amount; + int pendingCount = isBaseWearable ? 0 : pendingTransferService.GetPendingCount(urn); + int displayAmount = totalOwned - pendingCount; + backpackItemView.IsPendingTransfer = !isBaseWearable && displayAmount <= 0; + backpackItemView.NftCount = isBaseWearable ? 0 : Math.Max(displayAmount, 0); backpackItemView.SetEquipButtonsState(); - backpackItemView.SmartWearableBadgeContainer.SetActive(false); InitializeItemViewAsync(wearable, backpackItemView, pageFetchCancellationToken!.Token).Forget(); @@ -375,6 +392,25 @@ private async UniTaskVoid AwaitWearablesPromiseAsync(int pageNumber, bool refres currentSearch, results); + // DEBUG: Log what the API returned + ReportHub.Log(ReportCategory.GIFTING, $"[BackpackGrid] API returned {wearables.Count} wearables on page {pageNumber}"); + foreach (var w in wearables) + { + var urn = w.GetUrn(); + int apiAmount = w.Amount; + int registryCount = 0; + if (wearableStorage.TryGetOwnedNftRegistry(urn, out var registry)) + registryCount = registry.Count; + + bool isPending = pendingTransferService.IsPending(urn); + int pendingCount = pendingTransferService.GetPendingCount(urn); + + ReportHub.Log(ReportCategory.GIFTING, $"[BackpackGrid] Item: {urn} - Amount: {apiAmount}, RegistryCount: {registryCount}, IsPending: {isPending}, PendingCount: {pendingCount}"); + } + + // Prune stale pending wearable transfers that have been confirmed by the indexer + pendingTransferService.PruneWearables(wearableStorage.AllOwnedNftRegistry); + if (refreshPageSelector) pageSelectorController.Configure(totalAmount, CURRENT_PAGE_SIZE); @@ -420,6 +456,8 @@ private void ClearPoolElements() backpackItemView.Value.EquippedIcon.SetActive(false); backpackItemView.Value.IsEquipped = false; backpackItemView.Value.IsCompatibleWithBodyShape = true; + backpackItemView.Value.IsPendingTransfer = false; + backpackItemView.Value.NftCount = 0; backpackItemView.Value.ItemId = ""; gridItemsPool.Release(backpackItemView.Value); } @@ -447,6 +485,7 @@ private void OnEquip(IWearable equippedWearable, bool isManuallyEquipped) { if (usedPoolItems.TryGetValue(equippedWearable.GetUrn(), out BackpackItemView backpackItemView)) { + backpackItemView.EquippedIcon.SetActive(true); backpackItemView.IsEquipped = true; backpackItemView.SetEquipButtonsState(); } diff --git a/Explorer/Assets/DCL/Backpack/BackpackItemView.cs b/Explorer/Assets/DCL/Backpack/BackpackItemView.cs index ef96359843..9a3f666bad 100644 --- a/Explorer/Assets/DCL/Backpack/BackpackItemView.cs +++ b/Explorer/Assets/DCL/Backpack/BackpackItemView.cs @@ -4,6 +4,7 @@ using DG.Tweening; using System; using System.Threading; +using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; @@ -74,6 +75,14 @@ public class BackpackItemView : MonoBehaviour, IPointerEnterHandler, IPointerExi [SerializeField] private GameObject incompatibleWithBodyShapeContainer; [SerializeField] private GameObject incompatibleWithBodyShapeHoverContainer; + + [Header("Pending Transfer")] + [SerializeField] private GameObject pendingTransferContainer; + [SerializeField] private GameObject pendingTransferHoverContainer; + + [Header("NFT Count")] + [SerializeField] private GameObject nftCountContainer; + [SerializeField] private TextMeshProUGUI nftCountText; [field: SerializeField] public GameObject SmartWearableBadgeContainer { get; private set; } @@ -101,6 +110,32 @@ public bool IsCompatibleWithBodyShape isCompatibleWithBodyShape = value; } } + + public bool IsPendingTransfer + { + get => isPendingTransfer; + + set + { + isPendingTransfer = value; + if (pendingTransferContainer != null) + pendingTransferContainer.SetActive(value); + } + } + + public int NftCount + { + set + { + if (nftCountContainer != null) + { + bool showCount = value > 1; + nftCountContainer.SetActive(showCount); + if (showCount && nftCountText != null) + nftCountText.text = $"x{value}"; + } + } + } public bool CanHover { get; set; } = true; @@ -119,6 +154,7 @@ public bool IsLoading private CancellationTokenSource cts; private bool isCompatibleWithBodyShape; + private bool isPendingTransfer; private void Awake() { @@ -139,8 +175,8 @@ private void Awake() public void SetEquipButtonsState() { - EquipButton.gameObject.SetActive(!IsEquipped && IsCompatibleWithBodyShape); - UnEquipButton.gameObject.SetActive(IsEquipped && IsUnequippable); + EquipButton.gameObject.SetActive(!IsEquipped && IsCompatibleWithBodyShape && !IsPendingTransfer); + UnEquipButton.gameObject.SetActive(IsEquipped && IsUnequippable && !IsPendingTransfer); } private void OnDisable() @@ -162,6 +198,9 @@ public void OnPointerEnter(PointerEventData eventData) EquippedIcon.gameObject.SetActive(false); incompatibleWithBodyShapeHoverContainer.SetActive(!IsCompatibleWithBodyShape); + + if (pendingTransferHoverContainer != null) + pendingTransferHoverContainer.SetActive(IsPendingTransfer); } public void OnPointerExit(PointerEventData eventData) @@ -169,6 +208,9 @@ public void OnPointerExit(PointerEventData eventData) AnimateExit(); EquippedIcon.gameObject.SetActive(IsEquipped); incompatibleWithBodyShapeHoverContainer.SetActive(false); + + if (pendingTransferHoverContainer != null) + pendingTransferHoverContainer.SetActive(false); } public void OnPointerClick(PointerEventData eventData) @@ -185,7 +227,7 @@ public void OnPointerClick(PointerEventData eventData) UIAudioEventsBus.Instance.SendPlayAudioEvent(ClickAudio); break; case 2: - if (IsCompatibleWithBodyShape) + if (IsCompatibleWithBodyShape && !IsPendingTransfer) { OnEquip?.Invoke(Slot, ItemId); UIAudioEventsBus.Instance.SendPlayAudioEvent(EquipWearableAudio); diff --git a/Explorer/Assets/DCL/Backpack/EmotesSection/BackpackEmoteGridController.cs b/Explorer/Assets/DCL/Backpack/EmotesSection/BackpackEmoteGridController.cs index 81e6c10be6..f74b17a79b 100644 --- a/Explorer/Assets/DCL/Backpack/EmotesSection/BackpackEmoteGridController.cs +++ b/Explorer/Assets/DCL/Backpack/EmotesSection/BackpackEmoteGridController.cs @@ -8,6 +8,7 @@ using DCL.AvatarRendering.Wearables.Components; using DCL.AvatarRendering.Wearables.Helpers; using DCL.Backpack.BackpackBus; +using DCL.Backpack.Gifting.Services.PendingTransfers; using DCL.Browser; using DCL.CharacterPreview; using DCL.UI; @@ -51,6 +52,8 @@ public class BackpackEmoteGridController : IDisposable private readonly IThumbnailProvider thumbnailProvider; private readonly IWebBrowser webBrowser; private readonly IEmoteStorage emoteStorage; + private readonly IPendingTransferService pendingTransferService; + private readonly IWearableStorage wearableStorage; private readonly bool builderEmotesPreview; private CancellationTokenSource? loadElementsCancellationToken; @@ -76,7 +79,9 @@ public BackpackEmoteGridController( IThumbnailProvider thumbnailProvider, IWebBrowser webBrowser, IAppArgs appArgs, - IEmoteStorage emoteStorage) + IEmoteStorage emoteStorage, + IPendingTransferService pendingTransferService, + IWearableStorage wearableStorage) { this.view = view; this.commandBus = commandBus; @@ -92,6 +97,8 @@ public BackpackEmoteGridController( this.thumbnailProvider = thumbnailProvider; this.webBrowser = webBrowser; this.emoteStorage = emoteStorage; + this.pendingTransferService = pendingTransferService; + this.wearableStorage = wearableStorage; pageSelectorController = new PageSelectorController(view.PageSelectorView, pageButtonView); builderEmotesPreview = appArgs.HasFlag(AppArgsFlags.SELF_PREVIEW_BUILDER_COLLECTIONS); usedPoolItems = new Dictionary(); @@ -174,6 +181,9 @@ async UniTaskVoid RequestPageAsync(CancellationToken ct) customOwnedEmotes ); + // Prune stale pending emote transfers that have been confirmed by the indexer + pendingTransferService.PruneEmotes(emoteStorage.AllOwnedNftRegistry); + // TODO: request base emotes collection instead of pointers: // https://peer-ec1.decentraland.org/content/entities/active/collections/urn:decentraland:off-chain:base-avatars if (onChainEmotesOnly) @@ -307,9 +317,16 @@ private void SetGridElements(IReadOnlyList emotes) backpackItemView.IsCompatibleWithBodyShape = true; backpackItemView.EquippedSlotLabel.gameObject.SetActive(isEquipped); backpackItemView.EquippedSlotLabel.text = equippedSlot.ToString(); - + + var emote = emotes[i]; + bool isOnChain = emote.IsOnChain(); + int emoteAmount = emote.Amount; + int pendingCount = isOnChain ? pendingTransferService.GetPendingCount(emote.GetUrn()) : 0; + int displayAmount = emoteAmount - pendingCount; + backpackItemView.IsPendingTransfer = isOnChain && displayAmount <= 0; + backpackItemView.NftCount = isOnChain ? Math.Max(displayAmount, 0) : 0; backpackItemView.SetEquipButtonsState(); - WaitForThumbnailAsync(emotes[i], backpackItemView, loadElementsCancellationToken!.Token).Forget(); + WaitForThumbnailAsync(emote, backpackItemView, loadElementsCancellationToken!.Token).Forget(); } } @@ -359,6 +376,8 @@ private void ClearPoolElements() backpackItemView.Value.EquippedIcon.SetActive(false); backpackItemView.Value.IsEquipped = false; backpackItemView.Value.IsCompatibleWithBodyShape = false; + backpackItemView.Value.IsPendingTransfer = false; + backpackItemView.Value.NftCount = 0; backpackItemView.Value.ItemId = ""; gridItemsPool.Release(backpackItemView.Value); } @@ -385,6 +404,7 @@ private void OnUnequip(int slot, IEmote? emote) private void OnEquip(int slot, IEmote emote, bool _) { if (!usedPoolItems.TryGetValue(emote.GetUrn(), out BackpackEmoteGridItemView backpackItemView)) return; + backpackItemView.EquippedIcon.SetActive(true); backpackItemView.IsEquipped = true; backpackItemView.SetEquipButtonsState(); backpackItemView.EquippedSlotLabel.gameObject.SetActive(true); diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Debug/GiftingDiagnosticTool.cs b/Explorer/Assets/DCL/Backpack/Gifting/Debug/GiftingDiagnosticTool.cs new file mode 100644 index 0000000000..bff53b0c45 --- /dev/null +++ b/Explorer/Assets/DCL/Backpack/Gifting/Debug/GiftingDiagnosticTool.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; +using System.Text; +using Cysharp.Threading.Tasks; +using DCL.AvatarRendering.Emotes; +using DCL.AvatarRendering.Loading.Components; +using DCL.AvatarRendering.Wearables; +using DCL.AvatarRendering.Wearables.Components; +using DCL.AvatarRendering.Wearables.Helpers; +using DCL.Backpack.Gifting.Services.PendingTransfers; +using DCL.Diagnostics; +using DCL.Web3.Identities; +using UnityEngine; +using DCL.Backpack.Gifting.Utils; + +namespace DCL.Backpack.Gifting.Debug +{ + public class GiftingDiagnosticTool : MonoBehaviour + { + private IPendingTransferService pendingTransferService; + private IWearableStorage wearableStorage; + private IEmoteStorage emoteStorage; + private IWearablesProvider wearablesProvider; + private IEmoteProvider emoteProvider; + private IWeb3IdentityCache identityCache; + + public void Initialize( + IPendingTransferService pendingTransferService, + IWearableStorage wearableStorage, + IEmoteStorage emoteStorage, + IWearablesProvider wearablesProvider, + IEmoteProvider emoteProvider, + IWeb3IdentityCache identityCache) + { + this.pendingTransferService = pendingTransferService; + this.wearableStorage = wearableStorage; + this.emoteStorage = emoteStorage; + this.wearablesProvider = wearablesProvider; + this.emoteProvider = emoteProvider; + this.identityCache = identityCache; + } + + [ContextMenu("1. Print Pending Transfers")] + public void PrintPendingTransfers() + { + pendingTransferService.LogPendingTransfers(); + } + + [ContextMenu("2. Print Owned Wearables (Registry)")] + public void PrintWearableRegistry() + { + var sb = new StringBuilder(); + sb.AppendLine("=== Wearable Registry Dump ==="); + + // Accessing internal registry via interface might require casting or looking at exposed properties + // Assuming standard IWearableStorage usage: + foreach (var kvp in wearableStorage.AllOwnedNftRegistry) + { + string baseUrn = kvp.Key; + var instances = kvp.Value; + sb.AppendLine($"Base URN: {baseUrn} | Count: {instances.Count}"); + foreach (var instance in instances.Values) + { + sb.AppendLine($" - TokenId: {instance.TokenId} | Full URN: {instance.Urn}"); + } + } + ReportHub.Log(ReportCategory.GIFTING, sb.ToString()); + } + + [ContextMenu("3. Print Owned Emotes (Registry)")] + public void PrintEmoteRegistry() + { + var sb = new StringBuilder(); + sb.AppendLine("=== Emote Registry Dump ==="); + + foreach (var kvp in emoteStorage.AllOwnedNftRegistry) + { + string baseUrn = kvp.Key; + var instances = kvp.Value; + sb.AppendLine($"Base URN: {baseUrn} | Count: {instances.Count}"); + foreach (var instance in instances.Values) + { + sb.AppendLine($" - TokenId: {instance.TokenId} | Full URN: {instance.Urn}"); + } + } + ReportHub.Log(ReportCategory.GIFTING, sb.ToString()); + } + + [ContextMenu("4. Fetch & Log Backpack Wearables (API)")] + public void FetchBackpackWearables() + { + FetchWearablesAsync().Forget(); + } + + private async UniTaskVoid FetchWearablesAsync() + { + var sb = new StringBuilder(); + sb.AppendLine("=== Fetching Backpack Wearables (Page 1) ==="); + + var results = new List(); + + // Simulating Backpack Grid Request + await wearablesProvider.GetAsync( + pageSize: 20, + pageNumber: 1, + ct: System.Threading.CancellationToken.None, + results: results + // Add specific sorting/filtering if needed matches your backpack + ); + + foreach (var w in results) + { + sb.AppendLine($"[Wearable] Name: {w.GetName()} | URN: {w.GetUrn()} | Amount: {w.Amount}"); + } + + ReportHub.Log(ReportCategory.GIFTING, sb.ToString()); + } + + [ContextMenu("5. Fetch & Log Backpack Emotes (API)")] + public void FetchBackpackEmotes() + { + FetchEmotesAsync().Forget(); + } + + private async UniTaskVoid FetchEmotesAsync() + { + var identity = identityCache.Identity; + if (identity == null) + { + ReportHub.LogError(ReportCategory.GIFTING, "No identity found."); + return; + } + + var sb = new StringBuilder(); + sb.AppendLine("=== Fetching Backpack Emotes (Page 1) ==="); + + var results = new List(); + + // Simulating Backpack Emote Grid Request + await emoteProvider.GetOwnedEmotesAsync( + identity.Address, + System.Threading.CancellationToken.None, + new IEmoteProvider.OwnedEmotesRequestOptions( + pageNum: 1, + pageSize: 20, + collectionId: null, + orderOperation: new IEmoteProvider.OrderOperation("date", false), + name: "" + ), + results + ); + + foreach (var e in results) + { + bool isBase = GiftingUrnParsingHelper.TryGetBaseUrn(e.GetUrn(), out var baseUrn); + sb.AppendLine($"[Emote] Name: {e.GetName()} | URN: {e.GetUrn()}"); + sb.AppendLine($" -> IsOnChain: {e.IsOnChain()} | Parsed Base: {(isBase ? baseUrn : "N/A")}"); + } + + ReportHub.Log(ReportCategory.GIFTING, sb.ToString()); + } + + [ContextMenu("6. Prune Wearables")] + public void PruneWearables() + { + ReportHub.Log(ReportCategory.GIFTING, "=== Pruning Wearables ==="); + pendingTransferService.PruneWearables(wearableStorage.AllOwnedNftRegistry); + pendingTransferService.LogPendingTransfers(); + } + + [ContextMenu("7. Prune Emotes")] + public void PruneEmotes() + { + ReportHub.Log(ReportCategory.GIFTING, "=== Pruning Emotes ==="); + pendingTransferService.PruneEmotes(emoteStorage.AllOwnedNftRegistry); + pendingTransferService.LogPendingTransfers(); + } + + [ContextMenu("8. Prune All")] + public void PruneAll() + { + ReportHub.Log(ReportCategory.GIFTING, "=== Pruning All ==="); + pendingTransferService.PruneWearables(wearableStorage.AllOwnedNftRegistry); + pendingTransferService.PruneEmotes(emoteStorage.AllOwnedNftRegistry); + pendingTransferService.LogPendingTransfers(); + } + } +} diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Debug/GiftingDiagnosticTool.cs.meta b/Explorer/Assets/DCL/Backpack/Gifting/Debug/GiftingDiagnosticTool.cs.meta new file mode 100644 index 0000000000..221918417f --- /dev/null +++ b/Explorer/Assets/DCL/Backpack/Gifting/Debug/GiftingDiagnosticTool.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5cc873c7e38f43048d23d70075001f19 +timeCreated: 1769066705 diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftSelection/GiftSelectionController.cs b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftSelection/GiftSelectionController.cs index e93b4087fd..18c3902646 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftSelection/GiftSelectionController.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftSelection/GiftSelectionController.cs @@ -23,7 +23,6 @@ public class GiftSelectionController : ControllerBase CanvasOrdering.SortingLayer.Popup; - private readonly IProfileRepository profileRepository; private readonly GiftSelectionComponentFactory componentFactory; private readonly GiftInventoryService giftInventoryService; private readonly IAvatarEquippedStatusProvider equippedStatusProvider; @@ -44,13 +43,11 @@ public GiftSelectionController(ViewFactoryMethod viewFactory, GiftSelectionComponentFactory componentFactory, GiftInventoryService giftInventoryService, IAvatarEquippedStatusProvider equippedStatusProvider, - IProfileRepository profileRepository, IMVCManager mvcManager) : base(viewFactory) { this.componentFactory = componentFactory; this.giftInventoryService = giftInventoryService; this.equippedStatusProvider = equippedStatusProvider; - this.profileRepository = profileRepository; this.mvcManager = mvcManager; } @@ -203,6 +200,9 @@ private async UniTask OpenTransferPopupAsync() if (activePresenter == null || string.IsNullOrEmpty(selectedUrn)) return; + var ct = lifeCts?.Token ?? CancellationToken.None; + if (ct.IsCancellationRequested) return; + try { string itemType = activePresenter is WearableGridPresenter @@ -221,12 +221,8 @@ private async UniTask OpenTransferPopupAsync() if (!activePresenter.TryBuildStyleSnapshot(selectedUrn, out var style)) style = new GiftItemStyleSnapshot(null, null, Color.white); - - var ct = lifeCts?.Token ?? CancellationToken.None; - var recipientProfile = await profileRepository.GetAsync(inputData.userAddress, ct); - if (ct.IsCancellationRequested) return; - - var userNameColor = recipientProfile?.UserNameColor ?? Color.black; + + var userNameColor = inputData.userNameColor; string userNameColorHex = ColorUtility.ToHtmlStringRGB(userNameColor); var transferParams = new GiftTransferParams( @@ -244,6 +240,7 @@ private async UniTask OpenTransferPopupAsync() ); await mvcManager.ShowAsync(GiftTransferController.IssueCommand(transferParams), CancellationToken.None); + } catch (OperationCanceledException) { diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftSelection/GiftSelectionParams.cs b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftSelection/GiftSelectionParams.cs index ffa4e73ddd..2702f25fa0 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftSelection/GiftSelectionParams.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftSelection/GiftSelectionParams.cs @@ -1,14 +1,18 @@ -namespace DCL.Backpack.Gifting.Views +using UnityEngine; + +namespace DCL.Backpack.Gifting.Views { public struct GiftSelectionParams { public readonly string userAddress; public readonly string userName; + public readonly Color userNameColor; - public GiftSelectionParams(string userAddress, string userName) + public GiftSelectionParams(string userAddress, string userName, Color userNameColor) { - this.userAddress = userAddress; + this.userAddress = userAddress; this.userName = userName; + this.userNameColor = userNameColor; } } } \ No newline at end of file diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftTransfer/Commands/GiftTransferRequestCommand.cs b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftTransfer/Commands/GiftTransferRequestCommand.cs index 3c2fa892fd..55cbb25917 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftTransfer/Commands/GiftTransferRequestCommand.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/GiftTransfer/Commands/GiftTransferRequestCommand.cs @@ -1,7 +1,8 @@ -using System.Threading; +using System.Threading; using Cysharp.Threading.Tasks; using DCL.Backpack.Gifting.Events; using DCL.Backpack.Gifting.Services; +using DCL.Backpack.Gifting.Services.GiftingInventory; using DCL.Backpack.Gifting.Services.PendingTransfers; using DCL.Backpack.Gifting.Views; using DCL.Web3.Identities; @@ -68,7 +69,11 @@ public async UniTask ExecuteAsync(GiftTransferParams data, C if (result.IsSuccess) { - pendingTransferService.AddPending(data.instanceUrn); + // Add to appropriate pending list based on item type + if (data.itemType == GiftingItemTypes.Emote) + pendingTransferService.AddPendingEmote(data.instanceUrn); + else + pendingTransferService.AddPendingWearable(data.instanceUrn); eventBus.Publish(new GiftingEvents.GiftTransferSucceeded(data.giftUrn)); eventBus.Publish(new GiftingEvents.OnSuccessfulGift(data.giftUrn, senderAddress, data.recipientAddress, data.itemType)); diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/EmoteGridPresenter.cs b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/EmoteGridPresenter.cs index 55d2e0d08c..dfb3158518 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/EmoteGridPresenter.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/EmoteGridPresenter.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using Cysharp.Threading.Tasks; using DCL.AvatarRendering.Emotes; @@ -102,6 +102,11 @@ protected override GiftItemViewModel UpdateViewModelState(GiftItemViewModel vm, return vm.WithState(state, sprite); } + protected override void PrunePendingTransfers() + { + pendingTransferService.PruneEmotes(emoteStorage.AllOwnedNftRegistry); + } + public override bool TryBuildStyleSnapshot(string urn, out GiftItemStyleSnapshot style) { style = default; diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/GiftingGridPresenterBase.cs b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/GiftingGridPresenterBase.cs index 47d37e9bb2..faf058ece1 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/GiftingGridPresenterBase.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/GiftingGridPresenterBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Threading; using Cysharp.Threading.Tasks; @@ -163,8 +163,8 @@ private async UniTask RequestNextPageAsync(CancellationToken ct) (var items, int total) = await FetchDataAsync(CURRENT_PAGE_SIZE, currentPage, currentSearch, localCt); - pendingTransferService.Prune(wearableStorage.AllOwnedNftRegistry, - emoteStorage.AllOwnedNftRegistry); + // Prune pending transfers for this item type + PrunePendingTransfers(); totalCount = total; @@ -319,6 +319,7 @@ public CanvasGroup GetCanvasGroup() protected abstract int GetItemAmount(GiftableAvatarAttachment item); protected abstract void UpdateEmptyState(bool isEmpty); protected abstract TViewModel UpdateViewModelState(TViewModel vm, ThumbnailState state, Sprite? sprite); + protected abstract void PrunePendingTransfers(); public abstract bool TryBuildStyleSnapshot(string urn, out GiftItemStyleSnapshot style); } } \ No newline at end of file diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/WearableGridPresenter.cs b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/WearableGridPresenter.cs index e233bf321a..f894e3b3d8 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/WearableGridPresenter.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Presenters/Grid/WearableGridPresenter.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using Cysharp.Threading.Tasks; using DCL.AvatarRendering.Emotes; @@ -100,6 +100,11 @@ protected override GiftItemViewModel UpdateViewModelState(GiftItemViewModel vm, return vm.WithState(state, sprite); } + protected override void PrunePendingTransfers() + { + pendingTransferService.PruneWearables(wearableStorage.AllOwnedNftRegistry); + } + public override bool TryBuildStyleSnapshot(string urn, out GiftItemStyleSnapshot style) { style = default; diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/IGiftingPersistence.cs b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/IGiftingPersistence.cs index 46409818b9..58b2951c1b 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/IGiftingPersistence.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/IGiftingPersistence.cs @@ -1,10 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using DCL.Backpack.Gifting.Services.PendingTransfers; namespace DCL.Backpack.Gifting.Services { public interface IGiftingPersistence { - void SavePendingUrns(IEnumerable urns); - HashSet LoadPendingUrns(); + void SavePendingTransfers( + IEnumerable wearables, + IEnumerable emotes); + + (Dictionary wearables, Dictionary emotes) LoadPendingTransfers(); } -} \ No newline at end of file +} diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/IPendingTransferService.cs b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/IPendingTransferService.cs index 95f480cda8..40df2d7532 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/IPendingTransferService.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/IPendingTransferService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using CommunicationData.URLHelpers; using DCL.AvatarRendering.Wearables.Components; @@ -6,14 +6,43 @@ namespace DCL.Backpack.Gifting.Services.PendingTransfers { public interface IPendingTransferService { - void AddPending(string fullUrn); + /// + /// Add a wearable to pending transfers with current UTC timestamp. + /// + void AddPendingWearable(string fullUrn); + + /// + /// Add an emote to pending transfers with current UTC timestamp. + /// + void AddPendingEmote(string fullUrn); + + /// + /// Check if a URN (wearable or emote) is pending transfer. + /// bool IsPending(string fullUrn); + + /// + /// Get count of pending transfers for a base URN (checks both wearables and emotes). + /// int GetPendingCount(string baseUrn); - void Prune( - IReadOnlyDictionary> wearableRegistry, + /// + /// Prune wearable pending transfers based on registry state. + /// Call this after fetching wearables from API. + /// + void PruneWearables( + IReadOnlyDictionary> wearableRegistry); + + /// + /// Prune emote pending transfers based on registry state. + /// Call this after fetching emotes from API. + /// + void PruneEmotes( IReadOnlyDictionary> emoteRegistry); + /// + /// Log all pending transfers for debugging. + /// void LogPendingTransfers(); } -} \ No newline at end of file +} diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferEntry.cs b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferEntry.cs new file mode 100644 index 0000000000..52cce8f3c6 --- /dev/null +++ b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferEntry.cs @@ -0,0 +1,41 @@ +using System; + +namespace DCL.Backpack.Gifting.Services.PendingTransfers +{ + /// + /// Represents a pending transfer with timestamp for accurate pruning. + /// + public readonly struct PendingTransferEntry : IEquatable + { + /// + /// The full URN including token ID (e.g., urn:decentraland:matic:collections-v2:0x...:tokenId) + /// + public string FullUrn { get; } + + /// + /// UTC timestamp when the transfer was initiated. + /// Used to detect if an item returned after we sent it (A→B→A scenario). + /// + public DateTime SentAtUtc { get; } + + public PendingTransferEntry(string fullUrn, DateTime sentAtUtc) + { + FullUrn = fullUrn; + SentAtUtc = sentAtUtc; + } + + public PendingTransferEntry(string fullUrn) : this(fullUrn, DateTime.UtcNow) { } + + public bool Equals(PendingTransferEntry other) => + string.Equals(FullUrn, other.FullUrn, StringComparison.OrdinalIgnoreCase); + + public override bool Equals(object? obj) => + obj is PendingTransferEntry other && Equals(other); + + public override int GetHashCode() => + FullUrn?.ToLowerInvariant().GetHashCode() ?? 0; + + public override string ToString() => + $"PendingTransferEntry({FullUrn}, SentAt: {SentAtUtc:O})"; + } +} diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferEntry.cs.meta b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferEntry.cs.meta new file mode 100644 index 0000000000..013a5f7c9e --- /dev/null +++ b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferEntry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a7b3c5d8e9f0123456789abcdef01234 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferService.cs b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferService.cs index 6390764211..23779903fe 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferService.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PendingTransferService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; using CommunicationData.URLHelpers; using DCL.AvatarRendering.Wearables.Components; @@ -9,111 +10,251 @@ namespace DCL.Backpack.Gifting.Services.PendingTransfers { public class PendingTransferService : IPendingTransferService { + private const double TIMEOUT_HOURS = 1.0; + private readonly IGiftingPersistence persistence; - private readonly HashSet pendingFullUrns; + private readonly Dictionary pendingWearables; + private readonly Dictionary pendingEmotes; public PendingTransferService(IGiftingPersistence persistence) { this.persistence = persistence; - pendingFullUrns = persistence.LoadPendingUrns(); + + var (wearables, emotes) = persistence.LoadPendingTransfers(); + pendingWearables = wearables; + pendingEmotes = emotes; + + ReportHub.Log(ReportCategory.GIFTING, + $"[PendingTransferService] Loaded {pendingWearables.Count} wearables and {pendingEmotes.Count} emotes from disk."); + + foreach (var entry in pendingWearables.Values) + ReportHub.Log(ReportCategory.GIFTING, $" - Wearable: {entry}"); + foreach (var entry in pendingEmotes.Values) + ReportHub.Log(ReportCategory.GIFTING, $" - Emote: {entry}"); + } - ReportHub.Log(ReportCategory.GIFTING, $"[PendingTransferService] Loaded {pendingFullUrns.Count} items from disk."); - foreach (string? urn in pendingFullUrns) - ReportHub.Log(ReportCategory.GIFTING, $" - Loaded Pending: {urn}"); + public void AddPendingWearable(string fullUrn) + { + var entry = new PendingTransferEntry(fullUrn, DateTime.UtcNow); + + if (pendingWearables.TryAdd(fullUrn, entry)) + { + ReportHub.Log(ReportCategory.GIFTING, $"[PendingTransferService] Added pending wearable: {entry}"); + Save(); + } + else + { + ReportHub.Log(ReportCategory.GIFTING, $"[PendingTransferService] Wearable already pending: {fullUrn}"); + } } - public void AddPending(string fullUrn) + public void AddPendingEmote(string fullUrn) { - if (pendingFullUrns.Add(fullUrn)) + var entry = new PendingTransferEntry(fullUrn, DateTime.UtcNow); + + if (pendingEmotes.TryAdd(fullUrn, entry)) { - ReportHub.Log(ReportCategory.GIFTING, $"[PendingTransferService] Adding new pending item: {fullUrn}"); - persistence.SavePendingUrns(pendingFullUrns); + ReportHub.Log(ReportCategory.GIFTING, $"[PendingTransferService] Added pending emote: {entry}"); + Save(); } else { - ReportHub.Log(ReportCategory.GIFTING, $"[PendingTransferService] Item already exists in pending: {fullUrn}"); + ReportHub.Log(ReportCategory.GIFTING, $"[PendingTransferService] Emote already pending: {fullUrn}"); } } public bool IsPending(string fullUrn) { - return pendingFullUrns.Contains(fullUrn); + return pendingWearables.ContainsKey(fullUrn) || pendingEmotes.ContainsKey(fullUrn); } public int GetPendingCount(string baseUrn) { int count = 0; - foreach (string pending in pendingFullUrns) + + count += CountMatchingBase(pendingWearables.Values, baseUrn); + count += CountMatchingBase(pendingEmotes.Values, baseUrn); + + return count; + } + + private static int CountMatchingBase(IEnumerable entries, string baseUrn) + { + int count = 0; + foreach (var entry in entries) { - if (GiftingUrnParsingHelper.TryGetBaseUrn(pending, out string extractedBase) && - extractedBase == baseUrn) + if (GiftingUrnParsingHelper.TryGetBaseUrn(entry.FullUrn, out string extractedBase) && + string.Equals(extractedBase, baseUrn, StringComparison.OrdinalIgnoreCase)) { count++; } } - - ReportHub.Log(ReportCategory.GIFTING, $"[PendingTransferService] GetPendingCount for {baseUrn}: {count}"); return count; } - - public void Prune( - IReadOnlyDictionary> wearableRegistry, + public void PruneWearables( + IReadOnlyDictionary> wearableRegistry) + { + if (pendingWearables.Count == 0) return; + if (wearableRegistry.Count == 0) + { + ReportHub.Log(ReportCategory.GIFTING, "[Prune] Wearable registry empty, skipping prune."); + return; + } + + ReportHub.Log(ReportCategory.GIFTING, + $"[Prune] Pruning wearables. Pending: {pendingWearables.Count}, Registry entries: {wearableRegistry.Count}"); + + int pruned = PruneFromRegistry(pendingWearables, wearableRegistry, "Wearable"); + + if (pruned > 0) + { + Save(); + ReportHub.Log(ReportCategory.GIFTING, $"[Prune] Pruned {pruned} wearables."); + } + } + + public void PruneEmotes( IReadOnlyDictionary> emoteRegistry) { - if (pendingFullUrns.Count == 0) return; + if (pendingEmotes.Count == 0) return; + if (emoteRegistry.Count == 0) + { + ReportHub.Log(ReportCategory.GIFTING, "[Prune] Emote registry empty, skipping prune."); + return; + } - if (wearableRegistry.Count == 0 && - emoteRegistry.Count == 0) return; + ReportHub.Log(ReportCategory.GIFTING, + $"[Prune] Pruning emotes. Pending: {pendingEmotes.Count}, Registry entries: {emoteRegistry.Count}"); + int pruned = PruneFromRegistry(pendingEmotes, emoteRegistry, "Emote"); + + if (pruned > 0) + { + Save(); + ReportHub.Log(ReportCategory.GIFTING, $"[Prune] Pruned {pruned} emotes."); + } + } + + private static int PruneFromRegistry( + Dictionary pending, + IReadOnlyDictionary> registry, + string itemType) + { var toRemove = new List(); + var now = DateTime.UtcNow; - foreach (string pendingUrn in pendingFullUrns) + foreach (var (fullUrn, entry) in pending) { - if (!GiftingUrnParsingHelper.TryGetBaseUrn(pendingUrn, out string baseUrnString)) + // Rule 1: Safety timeout - prune if pending for more than 1 hour + double hoursPending = (now - entry.SentAtUtc).TotalHours; + if (hoursPending >= TIMEOUT_HOURS) + { + toRemove.Add(fullUrn); + ReportHub.Log(ReportCategory.GIFTING, + $"[Prune] {itemType} pruned by timeout ({hoursPending:F1}h): {fullUrn}"); + continue; + } + + // Parse base URN + if (!GiftingUrnParsingHelper.TryGetBaseUrn(fullUrn, out string baseUrnString)) { - toRemove.Add(pendingUrn); + toRemove.Add(fullUrn); + ReportHub.Log(ReportCategory.GIFTING, $"[Prune] {itemType} invalid URN format, removing: {fullUrn}"); continue; } var baseUrn = new URN(baseUrnString); - var fullUrnKey = new URN(pendingUrn); + var fullUrnKey = new URN(fullUrn); - bool stillOwned = false; + // Rule 2: Check if base URN exists in registry + if (!registry.TryGetValue(baseUrn, out var instances)) + { + // Base URN gone = user transferred their last copy of this item + toRemove.Add(fullUrn); + ReportHub.Log(ReportCategory.GIFTING, + $"[Prune] {itemType} base URN gone from registry: {fullUrn}"); + continue; + } - // O(1) Lookups - if (wearableRegistry.TryGetValue(baseUrn, out var wInstances) && wInstances.ContainsKey(fullUrnKey)) + // Rule 3: Check if specific token exists in registry + if (!TryFindInRegistry(instances, fullUrn, fullUrnKey, out var nftEntry)) { - stillOwned = true; + // Token gone = transfer confirmed + toRemove.Add(fullUrn); + ReportHub.Log(ReportCategory.GIFTING, + $"[Prune] {itemType} token gone from registry: {fullUrn}"); + continue; } - else if (emoteRegistry.TryGetValue(baseUrn, out var eInstances) && eInstances.ContainsKey(fullUrnKey)) + + // Rule 4: Token exists - check if it came back after we sent it (A→B→A scenario) + if (nftEntry.TransferredAt > entry.SentAtUtc) { - stillOwned = true; + toRemove.Add(fullUrn); + ReportHub.Log(ReportCategory.GIFTING, + $"[Prune] {itemType} returned after transfer (registry: {nftEntry.TransferredAt:O}, sent: {entry.SentAtUtc:O}): {fullUrn}"); + continue; } - // Validates pending transfers against the latest inventory data. - // If an item is no longer in the registry, it means the Indexer has caught up - // and the item has left the user's wallet. We can safely stop tracking it locally. - if (!stillOwned) - toRemove.Add(pendingUrn); + // Keep pending - transfer not yet confirmed by indexer + ReportHub.Log(ReportCategory.GIFTING, + $"[Prune] {itemType} still pending (sent {hoursPending:F2}h ago): {fullUrn}"); } - if (toRemove.Count > 0) - { - foreach (string item in toRemove) - pendingFullUrns.Remove(item); + // Apply removals + foreach (string urn in toRemove) + pending.Remove(urn); + + return toRemove.Count; + } - persistence.SavePendingUrns(pendingFullUrns); - ReportHub.Log(ReportCategory.GIFTING, $"Pruned {toRemove.Count} confirmed gifts."); + private static bool TryFindInRegistry( + IReadOnlyDictionary instances, + string pendingUrn, + URN fullUrnKey, + out NftBlockchainOperationEntry entry) + { + // Primary: URN-based lookup (O(1)) + if (instances.TryGetValue(fullUrnKey, out entry)) + return true; + + // Fallback: Normalized string comparison (O(n) but handles case differences) + string normalizedPending = pendingUrn.Trim().ToLowerInvariant(); + foreach (var kvp in instances) + { + string normalizedKey = kvp.Key.ToString().Trim().ToLowerInvariant(); + if (normalizedKey == normalizedPending) + { + entry = kvp.Value; + ReportHub.Log(ReportCategory.GIFTING, + $"[Prune] Fallback match found! Registry key '{kvp.Key}' matches pending '{pendingUrn}'"); + return true; + } } + + entry = default; + return false; } public void LogPendingTransfers() { var sb = new StringBuilder(); - sb.AppendLine("Pending Transfers:"); - foreach (string? urn in pendingFullUrns) sb.AppendLine(urn); + sb.AppendLine("=== Pending Transfers ==="); + + sb.AppendLine($"Wearables ({pendingWearables.Count}):"); + foreach (var entry in pendingWearables.Values) + sb.AppendLine($" - {entry}"); + + sb.AppendLine($"Emotes ({pendingEmotes.Count}):"); + foreach (var entry in pendingEmotes.Values) + sb.AppendLine($" - {entry}"); + ReportHub.Log(ReportCategory.GIFTING, sb.ToString()); } + + private void Save() + { + persistence.SavePendingTransfers(pendingWearables.Values, pendingEmotes.Values); + } } -} \ No newline at end of file +} diff --git a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PlayerPrefsGiftingPerstistence.cs b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PlayerPrefsGiftingPerstistence.cs index a77907003f..6190ac633c 100644 --- a/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PlayerPrefsGiftingPerstistence.cs +++ b/Explorer/Assets/DCL/Backpack/Gifting/Services/PendingTransfers/PlayerPrefsGiftingPerstistence.cs @@ -1,31 +1,124 @@ -using System; +using System; using System.Collections.Generic; +using System.Globalization; +using DCL.Backpack.Gifting.Services.PendingTransfers; +using DCL.Diagnostics; using DCL.Prefs; namespace DCL.Backpack.Gifting.Services { public class PlayerPrefsGiftingPersistence : IGiftingPersistence { - public void SavePendingUrns(IEnumerable urns) + private const char ENTRY_SEPARATOR = ';'; + private const char FIELD_SEPARATOR = '|'; + private const string DATE_FORMAT = "O"; // ISO 8601 round-trip format + + public void SavePendingTransfers( + IEnumerable wearables, + IEnumerable emotes) { - string data = string.Join(';', urns); - DCLPlayerPrefs.SetString(DCLPrefKeys.GIFTING_PENDING_GIFTS, data); + string wearablesData = SerializeEntries(wearables); + string emotesData = SerializeEntries(emotes); + + DCLPlayerPrefs.SetString(DCLPrefKeys.GIFTING_PENDING_WEARABLES_V2, wearablesData); + DCLPlayerPrefs.SetString(DCLPrefKeys.GIFTING_PENDING_EMOTES_V2, emotesData); DCLPlayerPrefs.Save(); } - public HashSet LoadPendingUrns() + public (Dictionary wearables, Dictionary emotes) LoadPendingTransfers() + { + // Check for legacy data and migrate if needed + if (DCLPlayerPrefs.HasKey(DCLPrefKeys.GIFTING_PENDING_GIFTS)) + { + var migrated = MigrateLegacyData(); + // Delete legacy key after migration + DCLPlayerPrefs.DeleteKey(DCLPrefKeys.GIFTING_PENDING_GIFTS); + DCLPlayerPrefs.Save(); + return migrated; + } + + var wearables = LoadEntries(DCLPrefKeys.GIFTING_PENDING_WEARABLES_V2); + var emotes = LoadEntries(DCLPrefKeys.GIFTING_PENDING_EMOTES_V2); + + return (wearables, emotes); + } + + private static string SerializeEntries(IEnumerable entries) + { + var parts = new List(); + foreach (var entry in entries) + { + string dateStr = entry.SentAtUtc.ToString(DATE_FORMAT, CultureInfo.InvariantCulture); + parts.Add($"{entry.FullUrn}{FIELD_SEPARATOR}{dateStr}"); + } + return string.Join(ENTRY_SEPARATOR, parts); + } + + private static Dictionary LoadEntries(string prefKey) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (!DCLPlayerPrefs.HasKey(prefKey)) + return result; + + string savedData = DCLPlayerPrefs.GetString(prefKey); + if (string.IsNullOrEmpty(savedData)) + return result; + + string[] entries = savedData.Split(ENTRY_SEPARATOR, StringSplitOptions.RemoveEmptyEntries); + foreach (string entryStr in entries) + { + int separatorIndex = entryStr.LastIndexOf(FIELD_SEPARATOR); + if (separatorIndex <= 0) + { + ReportHub.LogWarning(ReportCategory.GIFTING, $"[Persistence] Invalid entry format, skipping: {entryStr}"); + continue; + } + + string urn = entryStr.Substring(0, separatorIndex); + string dateStr = entryStr.Substring(separatorIndex + 1); + + if (!DateTime.TryParse(dateStr, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime sentAt)) + { + ReportHub.LogWarning(ReportCategory.GIFTING, $"[Persistence] Could not parse date, using UtcNow: {dateStr}"); + sentAt = DateTime.UtcNow; + } + + result[urn] = new PendingTransferEntry(urn, sentAt); + } + + return result; + } + + private (Dictionary wearables, Dictionary emotes) MigrateLegacyData() { - var result = new HashSet(); - if (!DCLPlayerPrefs.HasKey(DCLPrefKeys.GIFTING_PENDING_GIFTS)) return result; + ReportHub.Log(ReportCategory.GIFTING, "[Persistence] Migrating legacy pending gifts data..."); + + var wearables = new Dictionary(StringComparer.OrdinalIgnoreCase); + var emotes = new Dictionary(StringComparer.OrdinalIgnoreCase); + string savedData = DCLPlayerPrefs.GetString(DCLPrefKeys.GIFTING_PENDING_GIFTS); + if (string.IsNullOrEmpty(savedData)) + return (wearables, emotes); + + // Legacy format: "urn1;urn2;urn3" (no timestamps, no type distinction) + string[] urns = savedData.Split(ENTRY_SEPARATOR, StringSplitOptions.RemoveEmptyEntries); - if (string.IsNullOrEmpty(savedData)) return result; + // Assign current time as sentAt - the 1-hour timeout will naturally clean these up + // Assume all legacy entries are wearables (emote gifting is newer feature) + var now = DateTime.UtcNow; + foreach (string urn in urns) + { + wearables[urn] = new PendingTransferEntry(urn, now); + ReportHub.Log(ReportCategory.GIFTING, $"[Persistence] Migrated legacy entry as wearable: {urn}"); + } - string[]? split = savedData.Split(';', StringSplitOptions.RemoveEmptyEntries); - foreach (string part in split) - result.Add(part); + // Save migrated data to new format + SavePendingTransfers(wearables.Values, emotes.Values); - return result; + ReportHub.Log(ReportCategory.GIFTING, $"[Persistence] Migration complete. Migrated {wearables.Count} wearables."); + + return (wearables, emotes); } } -} \ No newline at end of file +} diff --git a/Explorer/Assets/DCL/Communities/CommunitiesCard/Members/MembersListController.cs b/Explorer/Assets/DCL/Communities/CommunitiesCard/Members/MembersListController.cs index cda32554e5..822f9d69ab 100644 --- a/Explorer/Assets/DCL/Communities/CommunitiesCard/Members/MembersListController.cs +++ b/Explorer/Assets/DCL/Communities/CommunitiesCard/Members/MembersListController.cs @@ -127,7 +127,7 @@ private void OnGiftUserRequested(ICommunityMemberData memberData) ReportHub.Log(ReportCategory.GIFTING, $"Gifting user: {memberData.Address}"); mvcManager.ShowAsync(GiftSelectionController - .IssueCommand(new GiftSelectionParams(memberData.Address, memberData.Name))).Forget(); + .IssueCommand(new GiftSelectionParams(memberData.Address, memberData.Name,memberData.GetUserNameColor()))).Forget(); } public override void Dispose() diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index 715ab436a4..e9b5e571db 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -350,7 +350,21 @@ static IMultiPool MultiPoolFactory() => var wearablesProvider = new ApplicationParametersWearablesProvider(appArgs, new ECSWearablesProvider(identityCache, globalWorld), builderDTOsURL.Value); - + + var giftingDiagnosticToolGO = new GameObject("Gifting_Diagnostic_Tool"); + Object.DontDestroyOnLoad(giftingDiagnosticToolGO); + + var diagnosticTool = giftingDiagnosticToolGO.AddComponent(); + + diagnosticTool.Initialize( + pendingTransferService, + wearableCatalog, + emotesCache, + wearablesProvider, + emoteProvider, + identityCache + ); + //TODO should be unified with LaunchMode bool localSceneDevelopment = !string.IsNullOrEmpty(dynamicWorldParams.LocalSceneDevelopmentRealm); bool builderCollectionsPreview = appArgs.HasFlag(AppArgsFlags.SELF_PREVIEW_BUILDER_COLLECTIONS); @@ -890,6 +904,7 @@ await MapRendererContainer bootstrapContainer.Analytics!, communitiesDataService, staticContainer.LoadingStatus, + pendingTransferService, donationsService, realmNavigator ), diff --git a/Explorer/Assets/DCL/Multiplayer/SDK/Tests/AvatarEmoteCommandPropagationSystemShould.cs b/Explorer/Assets/DCL/Multiplayer/SDK/Tests/AvatarEmoteCommandPropagationSystemShould.cs index f862d26454..6e2de67ca6 100644 --- a/Explorer/Assets/DCL/Multiplayer/SDK/Tests/AvatarEmoteCommandPropagationSystemShould.cs +++ b/Explorer/Assets/DCL/Multiplayer/SDK/Tests/AvatarEmoteCommandPropagationSystemShould.cs @@ -159,6 +159,11 @@ public void ClearOwnedNftRegistry() throw new NotImplementedException(); } + public void ClearOwnedNftForUrn(URN nftUrn) + { + throw new NotImplementedException(); + } + public bool TryGetLatestTransferredAt(URN nftUrn, out DateTime latestTransferredAt) { throw new NotImplementedException(); diff --git a/Explorer/Assets/DCL/Notifications/Assets/GiftNotification.prefab b/Explorer/Assets/DCL/Notifications/Assets/GiftNotification.prefab index 177726056c..e37e7b5905 100644 --- a/Explorer/Assets/DCL/Notifications/Assets/GiftNotification.prefab +++ b/Explorer/Assets/DCL/Notifications/Assets/GiftNotification.prefab @@ -1150,7 +1150,7 @@ GameObject: m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 - m_IsActive: 1 + m_IsActive: 0 --- !u!224 &156379979973073561 RectTransform: m_ObjectHideFlags: 0 diff --git a/Explorer/Assets/DCL/Passport/PassportController.cs b/Explorer/Assets/DCL/Passport/PassportController.cs index a42b40215e..727136d9e5 100644 --- a/Explorer/Assets/DCL/Passport/PassportController.cs +++ b/Explorer/Assets/DCL/Passport/PassportController.cs @@ -843,7 +843,9 @@ private async UniTaskVoid GiftUserClickedAsync() ReportHub.Log(ReportCategory.GIFTING, $"Gifting user: {inputData.UserId}"); // Open gifting popup and close passport popup - await mvcManager.ShowAsync(GiftSelectionController.IssueCommand(new GiftSelectionParams(targetProfile?.UserId, targetProfile?.DisplayName))); + await mvcManager.ShowAsync(GiftSelectionController.IssueCommand(new GiftSelectionParams(targetProfile.UserId, + targetProfile.DisplayName, + targetProfile.UserNameColor))); } private void BlockUserClicked() diff --git a/Explorer/Assets/DCL/PluginSystem/Global/BackpackSubPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/BackpackSubPlugin.cs index 773aba08b3..7a71243071 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/BackpackSubPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/BackpackSubPlugin.cs @@ -13,6 +13,7 @@ using DCL.Backpack.BackpackBus; using DCL.Backpack.CharacterPreview; using DCL.Backpack.EmotesSection; +using DCL.Backpack.Gifting.Services.PendingTransfers; using DCL.Browser; using DCL.CharacterPreview; using DCL.FeatureFlags; @@ -73,6 +74,7 @@ internal class BackpackSubPlugin : IDisposable private readonly SmartWearableCache smartWearableCache; private readonly IMVCManager mvcManager; private readonly IDecentralandUrlsSource decentralandUrlsSource; + private readonly IPendingTransferService pendingTransferService; private BackpackBusController? busController; private BackpackEquipStatusController? backpackEquipStatusController; @@ -111,7 +113,8 @@ public BackpackSubPlugin( IEventBus eventBus, SmartWearableCache smartWearableCache, IMVCManager mvcManager, - IDecentralandUrlsSource decentralandUrlsSource) + IDecentralandUrlsSource decentralandUrlsSource, + IPendingTransferService pendingTransferService) { this.featureFlags = featureFlags; this.assetsProvisioner = assetsProvisioner; @@ -145,6 +148,7 @@ public BackpackSubPlugin( this.smartWearableCache = smartWearableCache; this.mvcManager = mvcManager; this.decentralandUrlsSource = decentralandUrlsSource; + this.pendingTransferService = pendingTransferService; backpackCommandBus = new BackpackCommandBus(); } @@ -238,12 +242,15 @@ internal async UniTask InitializeAsync( bodyshapeColors, wearableStorage, smartWearableCache, - mvcManager + mvcManager, + pendingTransferService, + emoteStorage ); var emoteGridController = new BackpackEmoteGridController(emoteView.GridView, backpackCommandBus, backpackEventBus, web3Identity, rarityBackgroundsMapping, rarityColorMappings, categoryIconsMapping, equippedEmotes, - sortController, pageButtonView, emoteGridPool, emoteProvider, this.thumbnailProvider, webBrowser, appArgs, emoteStorage); + sortController, pageButtonView, emoteGridPool, emoteProvider, this.thumbnailProvider, webBrowser, appArgs, emoteStorage, + pendingTransferService, wearableStorage); var emotesController = new EmotesController(emoteView, new BackpackEmoteSlotsController(emoteView.Slots, backpackEventBus, backpackCommandBus, rarityBackgroundsMapping), emoteGridController); @@ -298,7 +305,8 @@ internal async UniTask InitializeAsync( nftNamesProvider, eventBus, deleteIcon, - decentralandUrlsSource + decentralandUrlsSource, + pendingTransferService ); } diff --git a/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs index 4ad4e8a239..dcec864167 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs @@ -1,679 +1,684 @@ -using Arch.Core; -using Arch.SystemGroups; -using CommunicationData.URLHelpers; -using Cysharp.Threading.Tasks; -using DCL.AssetsProvision; -using DCL.Audio; -using DCL.AvatarRendering.Emotes; -using DCL.AvatarRendering.Emotes.Equipped; -using DCL.AvatarRendering.Wearables; -using DCL.AvatarRendering.Wearables.Equipped; -using DCL.AvatarRendering.Wearables.Helpers; -using DCL.AvatarRendering.Wearables.ThirdParty; -using DCL.Backpack; -using DCL.Backpack.BackpackBus; -using DCL.Browser; -using DCL.CharacterPreview; -using DCL.Chat.EventBus; -using DCL.ExplorePanel; -using DCL.Input; -using DCL.Landscape.Settings; -using DCL.MapRenderer; -using DCL.Navmap; -using DCL.PlacesAPIService; -using DCL.Profiles; -using DCL.Profiles.Self; -using DCL.Quality; -using DCL.Settings; -using DCL.Settings.Configuration; -using DCL.UI.ProfileElements; -using DCL.UserInAppInitializationFlow; -using DCL.Web3.Authenticators; -using DCL.Web3.Identities; -using DCL.WebRequests; -using ECS; -using ECS.Prioritization; -using Global.Dynamic; -using MVC; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using DCL.Backpack.AvatarSection.Outfits.Repository; -using DCL.Chat.MessageBus; -using DCL.Clipboard; -using DCL.Communities; -using DCL.Communities.CommunitiesBrowser; -using DCL.Communities.CommunitiesDataProvider; -using DCL.Donations; -using DCL.EventsApi; -using DCL.FeatureFlags; -using DCL.Friends.UserBlocking; -using DCL.InWorldCamera; -using DCL.Navmap.ScriptableObjects; -using DCL.InWorldCamera.CameraReelGallery; -using DCL.InWorldCamera.CameraReelGallery.Components; -using DCL.InWorldCamera.CameraReelStorageService; -using DCL.MapRenderer.MapLayers.HomeMarker; -using DCL.Multiplayer.Connections.DecentralandUrls; -using DCL.Optimization.PerformanceBudgeting; -using DCL.Passport; -using DCL.PerformanceAndDiagnostics.Analytics; -using DCL.Places; -using DCL.RealmNavigation; -using DCL.UI.Profiles.Helpers; -using DCL.SDKComponents.MediaStream.Settings; -using DCL.Settings.Settings; -using DCL.SkyBox; -using DCL.UI; -using DCL.UI.Profiles; -using DCL.UI.SharedSpaceManager; -using DCL.Utilities; -using Utility; -using DCL.VoiceChat; -using ECS.SceneLifeCycle.IncreasingRadius; -using ECS.SceneLifeCycle.Realm; -using Global.AppArgs; -using Runtime.Wearables; -using UnityEngine; -using UnityEngine.AddressableAssets; -using UnityEngine.Audio; -using UnityEngine.Pool; -using Object = UnityEngine.Object; - -// ReSharper disable UnusedAutoPropertyAccessor.Local -namespace DCL.PluginSystem.Global -{ - public class ExplorePanelPlugin : IDCLGlobalPlugin - { - private readonly IEventBus eventBus; - private readonly FeatureFlagsConfiguration featureFlags; - private readonly IAssetsProvisioner assetsProvisioner; - private readonly MapRendererContainer mapRendererContainer; - private readonly IMVCManager mvcManager; - private readonly IPlacesAPIService placesAPIService; - private readonly IProfileRepository profileRepository; - private readonly IUserInAppInitializationFlow userInAppInitializationFlow; - private readonly ISelfProfile selfProfile; - private readonly IEquippedWearables equippedWearables; - private readonly IEquippedEmotes equippedEmotes; - private readonly IWeb3Authenticator web3Authenticator; - private readonly IWeb3IdentityCache web3IdentityCache; - private readonly ICameraReelStorageService cameraReelStorageService; - private readonly ICameraReelScreenshotsStorage cameraReelScreenshotsStorage; - private readonly ISystemClipboard systemClipboard; - private readonly IDecentralandUrlsSource decentralandUrlsSource; - private readonly IWearableStorage wearableStorage; - private readonly ICharacterPreviewFactory characterPreviewFactory; - private readonly IWebBrowser webBrowser; - private readonly IEmoteStorage emoteStorage; - private readonly IWebRequestController webRequestController; - private readonly CharacterPreviewEventBus characterPreviewEventBus; - private readonly IBackpackEventBus backpackEventBus; - private readonly IThirdPartyNftProviderSource thirdPartyNftProviderSource; - private readonly IWearablesProvider wearablesProvider; - private readonly ICursor cursor; - private readonly IEmoteProvider emoteProvider; - private readonly Arch.Core.World world; - private readonly Entity playerEntity; - private readonly IMapPathEventBus mapPathEventBus; - private readonly IRealmData realmData; - private readonly IProfileCache profileCache; - private readonly URLDomain assetBundleURL; - private readonly IInputBlock inputBlock; - private readonly IChatMessagesBus chatMessagesBus; - private readonly ISystemMemoryCap systemMemoryCap; - private readonly VolumeBus volumeBus; - private readonly HttpEventsApiService eventsApiService; - private readonly GoogleUserCalendar userCalendar; - private readonly ISystemClipboard clipboard; - private readonly ObjectProxy explorePanelNavmapBus; - private readonly IAppArgs appArgs; - private readonly ObjectProxy userBlockingCacheProxy; - private readonly ISharedSpaceManager sharedSpaceManager; - private readonly SceneLoadingLimit sceneLoadingLimit; - private readonly WarningNotificationView inWorldWarningNotificationView; - private readonly ProfileChangesBus profileChangesBus; - private readonly CommunitiesDataProvider communitiesDataProvider; - private readonly INftNamesProvider nftNamesProvider; - private readonly IThumbnailProvider thumbnailProvider; - private readonly IChatEventBus chatEventBus; - private readonly HomePlaceEventBus homePlaceEventBus; - - private readonly bool includeCameraReel; - private readonly bool includeDiscover; - - private NavmapController? navmapController; - private SettingsController? settingsController; - private BackpackSubPlugin? backpackSubPlugin; - private CategoryFilterController? categoryFilterController; - private SearchResultPanelController? searchResultPanelController; - private PlacesAndEventsPanelController? placesAndEventsPanelController; - private NavmapView? navmapView; - private PlaceInfoPanelController? placeInfoPanelController; - private NavmapSearchBarController? searchBarController; - private EventInfoPanelController? eventInfoPanelController; - private readonly ProfileRepositoryWrapper profileRepositoryWrapper; - private readonly UpscalingController upscalingController; - private CommunitiesBrowserController? communitiesBrowserController; - private PlacesController? placesController; - private readonly bool isVoiceChatEnabled; - private readonly bool isTranslationChatEnabled; - private readonly GalleryEventBus galleryEventBus; - private readonly IVoiceChatOrchestrator communityCallOrchestrator; - private readonly IPassportBridge passportBridge; - private readonly SmartWearableCache smartWearableCache; - private readonly IAnalyticsController analytics; - private readonly CommunityDataService communityDataService; - private readonly ILoadingStatus loadingStatus; - private readonly ImageControllerProvider imageControllerProvider; - private readonly IDonationsService donationsService; - private readonly IRealmNavigator realmNavigator; - - public ExplorePanelPlugin(IEventBus eventBus, - FeatureFlagsConfiguration featureFlags, - IAssetsProvisioner assetsProvisioner, - IMVCManager mvcManager, - MapRendererContainer mapRendererContainer, - IPlacesAPIService placesAPIService, - IWebRequestController webRequestController, - IWeb3IdentityCache web3IdentityCache, - ICameraReelStorageService cameraReelStorageService, - ICameraReelScreenshotsStorage cameraReelScreenshotsStorage, - ISystemClipboard systemClipboard, - IDecentralandUrlsSource decentralandUrlsSource, - IWearableStorage wearableStorage, - ICharacterPreviewFactory characterPreviewFactory, - IProfileRepository profileRepository, - IWeb3Authenticator web3Authenticator, - IUserInAppInitializationFlow userInAppInitializationFlow, - ISelfProfile selfProfile, - IEquippedWearables equippedWearables, - IEquippedEmotes equippedEmotes, - IWebBrowser webBrowser, - IEmoteStorage emoteStorage, - IRealmData realmData, - IProfileCache profileCache, - CharacterPreviewEventBus characterPreviewEventBus, - IMapPathEventBus mapPathEventBus, - IBackpackEventBus backpackEventBus, - IThirdPartyNftProviderSource thirdPartyNftProviderSource, - IWearablesProvider wearablesProvider, - ICursor cursor, - IInputBlock inputBlock, - IEmoteProvider emoteProvider, - Arch.Core.World world, - Entity playerEntity, - IChatMessagesBus chatMessagesBus, - ISystemMemoryCap systemMemoryCap, - VolumeBus volumeBus, - HttpEventsApiService eventsApiService, - GoogleUserCalendar userCalendar, - ISystemClipboard clipboard, - ObjectProxy explorePanelNavmapBus, - bool includeCameraReel, - bool includeDiscover, - IAppArgs appArgs, - ObjectProxy userBlockingCacheProxy, - ISharedSpaceManager sharedSpaceManager, - ProfileChangesBus profileChangesBus, - SceneLoadingLimit sceneLoadingLimit, - WarningNotificationView inWorldWarningNotificationView, - ProfileRepositoryWrapper profileDataProvider, - UpscalingController upscalingController, - CommunitiesDataProvider communitiesDataProvider, - INftNamesProvider nftNamesProvider, - IVoiceChatOrchestrator communityCallOrchestrator, - bool isTranslationChatEnabled, - GalleryEventBus galleryEventBus, - IThumbnailProvider thumbnailProvider, - IPassportBridge passportBridge, - IChatEventBus chatEventBus, - HomePlaceEventBus homePlaceEventBus, - SmartWearableCache smartWearableCache, - ImageControllerProvider imageControllerProvider, - IAnalyticsController analytics, - CommunityDataService communityDataService, - ILoadingStatus loadingStatus, - IDonationsService donationsService, - IRealmNavigator realmNavigator) - { - this.eventBus = eventBus; - this.featureFlags = featureFlags; - this.assetsProvisioner = assetsProvisioner; - this.mvcManager = mvcManager; - this.mapRendererContainer = mapRendererContainer; - this.placesAPIService = placesAPIService; - this.webRequestController = webRequestController; - this.web3IdentityCache = web3IdentityCache; - this.cameraReelStorageService = cameraReelStorageService; - this.cameraReelScreenshotsStorage = cameraReelScreenshotsStorage; - this.systemClipboard = systemClipboard; - this.decentralandUrlsSource = decentralandUrlsSource; - this.wearableStorage = wearableStorage; - this.characterPreviewFactory = characterPreviewFactory; - this.profileRepository = profileRepository; - this.web3Authenticator = web3Authenticator; - this.userInAppInitializationFlow = userInAppInitializationFlow; - this.selfProfile = selfProfile; - this.equippedWearables = equippedWearables; - this.equippedEmotes = equippedEmotes; - this.webBrowser = webBrowser; - this.realmData = realmData; - this.profileCache = profileCache; - this.emoteStorage = emoteStorage; - this.characterPreviewEventBus = characterPreviewEventBus; - this.mapPathEventBus = mapPathEventBus; - this.backpackEventBus = backpackEventBus; - this.thirdPartyNftProviderSource = thirdPartyNftProviderSource; - this.wearablesProvider = wearablesProvider; - this.inputBlock = inputBlock; - this.cursor = cursor; - this.emoteProvider = emoteProvider; - this.world = world; - this.playerEntity = playerEntity; - this.chatMessagesBus = chatMessagesBus; - this.systemMemoryCap = systemMemoryCap; - this.volumeBus = volumeBus; - this.eventsApiService = eventsApiService; - this.userCalendar = userCalendar; - this.clipboard = clipboard; - this.explorePanelNavmapBus = explorePanelNavmapBus; - this.includeCameraReel = includeCameraReel; - this.includeDiscover = includeDiscover; - this.appArgs = appArgs; - this.userBlockingCacheProxy = userBlockingCacheProxy; - this.sharedSpaceManager = sharedSpaceManager; - this.profileChangesBus = profileChangesBus; - this.sceneLoadingLimit = sceneLoadingLimit; - this.inWorldWarningNotificationView = inWorldWarningNotificationView; - this.profileRepositoryWrapper = profileDataProvider; - this.upscalingController = upscalingController; - this.communitiesDataProvider = communitiesDataProvider; - this.nftNamesProvider = nftNamesProvider; - this.isTranslationChatEnabled = isTranslationChatEnabled; - this.galleryEventBus = galleryEventBus; - this.communityCallOrchestrator = communityCallOrchestrator; - this.thumbnailProvider = thumbnailProvider; - this.chatEventBus = chatEventBus; - this.homePlaceEventBus = homePlaceEventBus; - this.passportBridge = passportBridge; - this.smartWearableCache = smartWearableCache; - this.imageControllerProvider = imageControllerProvider; - this.analytics = analytics; - this.communityDataService = communityDataService; - this.loadingStatus = loadingStatus; - this.donationsService = donationsService; - this.realmNavigator = realmNavigator; - } - - public void Dispose() - { - categoryFilterController?.Dispose(); - navmapController?.Dispose(); - settingsController?.Dispose(); - backpackSubPlugin?.Dispose(); - placeInfoPanelController?.Dispose(); - communitiesBrowserController?.Dispose(); - placesController?.Dispose(); - upscalingController?.Dispose(); - } - - public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in GlobalPluginArguments arguments) { } - - public async UniTask InitializeAsync(ExplorePanelSettings settings, CancellationToken ct) - { - INavmapBus navmapBus = new NavmapCommandBus(CreateSearchPlaceCommand, - CreateShowPlaceCommand, CreateShowEventCommand, placesAPIService); - - explorePanelNavmapBus.SetObject(navmapBus); - - var outfitsRepository = new OutfitsRepository(realmData, nftNamesProvider); - - backpackSubPlugin = new BackpackSubPlugin( - featureFlags, - assetsProvisioner, - web3IdentityCache, - characterPreviewFactory, - wearableStorage, - selfProfile, - profileCache, - equippedWearables, - equippedEmotes, - emoteStorage, - characterPreviewEventBus, - backpackEventBus, - thirdPartyNftProviderSource, - wearablesProvider, - inputBlock, - cursor, - emoteProvider, - world, - playerEntity, - appArgs, - webBrowser, - inWorldWarningNotificationView, - thumbnailProvider, - profileChangesBus, - outfitsRepository, - realmData, - webRequestController, - nftNamesProvider, - eventBus, - smartWearableCache, - mvcManager, - decentralandUrlsSource - ); - - ExplorePanelView panelViewAsset = (await assetsProvisioner.ProvideMainAssetValueAsync(settings.ExplorePanelPrefab, ct: ct)).GetComponent(); - ControllerBase.ViewFactoryMethod viewFactoryMethod = ExplorePanelController.Preallocate(panelViewAsset, null, out ExplorePanelView explorePanelView); - - ProvidedAsset generalAudioMixer = await assetsProvisioner.ProvideMainAssetAsync(settings.GeneralAudioMixer, ct); - - ProvidedAsset landscapeData = await assetsProvisioner.ProvideMainAssetAsync(settings.LandscapeData, ct); - - ProvidedAsset categoryMappingSO = await assetsProvisioner.ProvideMainAssetAsync(settings.CategoryMappingSO, ct); - - ProvidedAsset placeCategoriesSO = await assetsProvisioner.ProvideMainAssetAsync(settings.PlaceCategoriesSO, ct); - - navmapView = explorePanelView.GetComponentInChildren(); - categoryFilterController = new CategoryFilterController(navmapView.categoryToggles, mapRendererContainer.MapRenderer, navmapBus); - - NavmapZoomController zoomController = new (navmapView.zoomView, navmapBus); - - ObjectPool placeElementsPool = await InitializePlaceElementsPoolAsync(navmapView.SearchBarResultPanel, ct); - ObjectPool eventElementsPool = await InitializeEventElementsForPlacePoolAsync(navmapView.PlacesAndEventsPanelView.PlaceInfoPanelView, ct); - ObjectPool eventScheduleElementsPool = await InitializeEventScheduleElementsPoolAsync(navmapView.PlacesAndEventsPanelView.EventInfoPanelView, ct); - - searchResultPanelController = new SearchResultPanelController(navmapView.SearchBarResultPanel, - placeElementsPool, navmapBus); - - searchBarController = new NavmapSearchBarController(navmapView.SearchBarView, - navmapView.HistoryRecordPanelView, navmapView.PlacesAndEventsPanelView.SearchFiltersView, - inputBlock, navmapBus, categoryMappingSO.Value); - - SharePlacesAndEventsContextMenuController shareContextMenu = new (navmapView.ShareContextMenuView, - navmapView.WorldsWarningNotificationView, clipboard, webBrowser); - - placeInfoPanelController = new PlaceInfoPanelController(navmapView.PlacesAndEventsPanelView.PlaceInfoPanelView, - imageControllerProvider, placesAPIService, mapPathEventBus, navmapBus, chatMessagesBus, eventsApiService, - eventElementsPool, shareContextMenu, webBrowser, mvcManager, homePlaceEventBus, donationsService, cameraReelStorageService, cameraReelScreenshotsStorage, - new ReelGalleryConfigParams( - settings.PlaceGridLayoutFixedColumnCount, - settings.PlaceThumbnailHeight, - settings.PlaceThumbnailWidth, - false, - false), - false, - galleryEventBus: galleryEventBus); - - eventInfoPanelController = new EventInfoPanelController(navmapView.PlacesAndEventsPanelView.EventInfoPanelView, - navmapBus, chatMessagesBus, eventsApiService, eventScheduleElementsPool, - userCalendar, shareContextMenu, webBrowser, imageControllerProvider); - - placesAndEventsPanelController = new PlacesAndEventsPanelController(navmapView.PlacesAndEventsPanelView, - searchBarController, searchResultPanelController, placeInfoPanelController, eventInfoPanelController, - zoomController); - - IMapRenderer mapRenderer = mapRendererContainer.MapRenderer; - - SatelliteController satelliteController = new (navmapView.GetComponentInChildren(), - navmapView.MapCameraDragBehaviorData, mapRenderer, webBrowser); - - PlaceInfoToastController placeToastController = new (navmapView.PlaceToastView, - new PlaceInfoPanelController(navmapView.PlaceToastView.PlacePanelView, - imageControllerProvider, placesAPIService, mapPathEventBus, navmapBus, chatMessagesBus, eventsApiService, - eventElementsPool, shareContextMenu, webBrowser, mvcManager, homePlaceEventBus, donationsService, galleryEventBus: galleryEventBus), - placesAPIService, eventsApiService, navmapBus); - - settingsController = new SettingsController( - explorePanelView.GetComponentInChildren(), - settings.SettingsMenuConfiguration, - generalAudioMixer.Value, - settings.RealmPartitionSettings, - settings.VideoPrioritizationSettings, - landscapeData.Value, - settings.QualitySettingsAsset, - settings.SkyboxSettingsAsset, - settings.ControlsSettingsAsset, - systemMemoryCap, - settings.ChatSettingsAsset, - userBlockingCacheProxy, - sceneLoadingLimit, - volumeBus, - upscalingController, - isTranslationChatEnabled, - assetsProvisioner, - eventBus, - appArgs); - - await settingsController.InitializeAsync(); - - navmapController = new NavmapController( - navmapView: explorePanelView.GetComponentInChildren(), - mapRendererContainer.MapRenderer, - realmData, - mapPathEventBus, - world, - playerEntity, - navmapBus, - UIAudioEventsBus.Instance, - placesAndEventsPanelController, - searchBarController, - zoomController, - satelliteController, - placesAPIService, - homePlaceEventBus); - - await backpackSubPlugin.InitializeAsync(settings.BackpackSettings, explorePanelView.GetComponentInChildren(), ct); - - CameraReelView cameraReelView = explorePanelView.GetComponentInChildren(); - - var cameraReelController = new CameraReelController(cameraReelView, - new CameraReelGalleryController( - cameraReelView.CameraReelGalleryView, - this.cameraReelStorageService, - cameraReelScreenshotsStorage, - new ReelGalleryConfigParams(settings.GridLayoutFixedColumnCount, settings.ThumbnailHeight, settings.ThumbnailWidth, true, true), true, - galleryEventBus, - cameraReelView.CameraReelOptionsButton, - webBrowser, decentralandUrlsSource, systemClipboard, - settings.CameraReelGalleryMessages, - mvcManager), - cameraReelStorageService, - web3IdentityCache, - mvcManager, - cursor, - galleryEventBus, - settings.StorageProgressBarText); - - CommunitiesBrowserView communitiesBrowserView = explorePanelView.GetComponentInChildren(); - - communitiesBrowserController = new CommunitiesBrowserController( - communitiesBrowserView, - cursor, - communitiesDataProvider, - webRequestController, - inputBlock, - mvcManager, - profileRepositoryWrapper, - selfProfile, - nftNamesProvider, - communityCallOrchestrator, - sharedSpaceManager, - chatEventBus, - analytics, - communityDataService, - loadingStatus); - - PlacesView placesView = explorePanelView.GetComponentInChildren(); - placesController = new PlacesController(placesView, cursor, placesAPIService, placeCategoriesSO.Value, inputBlock, selfProfile, webBrowser, webRequestController, realmNavigator, clipboard, decentralandUrlsSource); - - ExplorePanelController explorePanelController = new - ExplorePanelController(viewFactoryMethod, - navmapController, - settingsController, - backpackSubPlugin.backpackController!, - cameraReelController, - new ProfileWidgetController(() => explorePanelView.ProfileWidget, - web3IdentityCache, - profileRepository, - profileChangesBus), - new ProfileMenuController(() => explorePanelView.ProfileMenuView, - web3IdentityCache, - profileRepository, - world, - playerEntity, - webBrowser, - web3Authenticator, - userInAppInitializationFlow, - profileCache, - passportBridge, - profileRepositoryWrapper), - communitiesBrowserController, - placesController, - inputBlock, - includeCameraReel, - includeDiscover, - sharedSpaceManager); - - sharedSpaceManager.RegisterPanel(PanelsSharingSpace.Explore, explorePanelController); - mvcManager.RegisterController(explorePanelController); - } - - private async UniTask> InitializePlaceElementsPoolAsync(SearchResultPanelView view, CancellationToken ct) - { - PlaceElementView asset = (await assetsProvisioner.ProvideInstanceAsync(view.ResultRef, ct: ct)).Value; - - return new ObjectPool( - () => CreatePoolElements(asset), - actionOnGet: result => result.gameObject.SetActive(true), - actionOnRelease: result => result.gameObject.SetActive(false), - defaultCapacity: 8 - ); - - PlaceElementView CreatePoolElements(PlaceElementView asset) - { - PlaceElementView placeElementView = Object.Instantiate(asset, view.searchResultsContainer); - placeElementView.ConfigurePlaceImageController(imageControllerProvider); - return placeElementView; - } - } - - private async UniTask> InitializeEventElementsForPlacePoolAsync(PlaceInfoPanelView view, CancellationToken ct) - { - EventElementView asset = (await assetsProvisioner.ProvideInstanceAsync(view.EventElementViewRef, ct: ct)).Value; - - return new ObjectPool( - () => CreatePoolElements(asset), - actionOnGet: result => result.gameObject.SetActive(true), - actionOnRelease: result => result.gameObject.SetActive(false), - defaultCapacity: 8 - ); - - EventElementView CreatePoolElements(EventElementView asset) - { - EventElementView placeElementView = Object.Instantiate(asset, view.EventsContentContainer.transform); - return placeElementView; - } - } - - private async UniTask> InitializeEventScheduleElementsPoolAsync(EventInfoPanelView view, CancellationToken ct) - { - EventScheduleElementView asset = (await assetsProvisioner.ProvideInstanceAsync(view.ScheduleElementRef, ct: ct)).Value; - - return new ObjectPool( - () => CreatePoolElements(asset), - actionOnGet: result => result.gameObject.SetActive(true), - actionOnRelease: result => result.gameObject.SetActive(false), - defaultCapacity: 8 - ); - - EventScheduleElementView CreatePoolElements(EventScheduleElementView asset) - { - EventScheduleElementView placeElementView = Object.Instantiate(asset, view.ScheduleElementsContainer); - return placeElementView; - } - } - - private INavmapCommand CreateSearchPlaceCommand(INavmapBus.SearchPlaceResultDelegate callback, INavmapBus.SearchPlaceParams @params) => - new SearchForPlaceAndShowResultsCommand(placesAPIService, eventsApiService, placesAndEventsPanelController!, - searchResultPanelController!, searchBarController!, callback, - @params); - - private INavmapCommand CreateShowPlaceCommand(PlacesData.PlaceInfo placeInfo) => - new ShowPlaceInfoCommand(placeInfo, placeInfoPanelController!, placesAndEventsPanelController!, eventsApiService, - searchBarController!); - - private INavmapCommand CreateShowEventCommand(EventDTO @event, PlacesData.PlaceInfo? place = null) => - new ShowEventInfoCommand(@event, eventInfoPanelController!, placesAndEventsPanelController!, - searchBarController!, placesAPIService, place); - - public class ExplorePanelSettings : IDCLPluginSettings - { - [field: Header(nameof(ExplorePanelPlugin) + "." + nameof(ExplorePanelSettings))] - [field: Space] - [field: SerializeField] - public AssetReferenceGameObject ExplorePanelPrefab; - - [field: SerializeField] - public BackpackSettings BackpackSettings { get; private set; } - - [field: SerializeField] - public SettingsMenuConfiguration SettingsMenuConfiguration { get; private set; } - - [field: SerializeField] - public AssetReferenceT GeneralAudioMixer { get; private set; } - - [field: SerializeField] - public RealmPartitionSettingsAsset RealmPartitionSettings { get; private set; } - - [field: SerializeField] - public VideoPrioritizationSettings VideoPrioritizationSettings { get; private set; } - - [field: SerializeField] - public LandscapeDataRef LandscapeData { get; private set; } - - [field: SerializeField] - public QualitySettingsAsset QualitySettingsAsset { get; private set; } - [field: SerializeField] - public SkyboxSettingsAsset SkyboxSettingsAsset { get; private set; } - - [field: SerializeField] - public ControlsSettingsAsset ControlsSettingsAsset { get; private set; } - - [field: SerializeField] - public ChatSettingsAsset ChatSettingsAsset { get; private set; } - - [field: SerializeField] - public AssetReferenceT CategoryMappingSO { get; private set; } - - [field: Header("Camera Reel")] - [field: SerializeField] - [field: Tooltip("Spaces will be HTTP sanitized, care for special characters")] - public CameraReelGalleryMessagesConfiguration CameraReelGalleryMessages { get; private set; } - - [field: SerializeField] - public string StorageProgressBarText { get; private set; } - - [field: SerializeField] - public int GridLayoutFixedColumnCount { get; private set; } - [field: SerializeField] - public int ThumbnailHeight { get; private set; } - [field: SerializeField] - public int ThumbnailWidth { get; private set; } - - [field: Header("Place Reel")] - [field: SerializeField] - public int PlaceGridLayoutFixedColumnCount { get; private set; } - - [field: SerializeField] - public int PlaceThumbnailHeight { get; private set; } - - [field: SerializeField] - public int PlaceThumbnailWidth { get; private set; } - - [field: SerializeField] - public AssetReferenceT PlaceCategoriesSO { get; private set; } - } - } -} +using Arch.Core; +using Arch.SystemGroups; +using CommunicationData.URLHelpers; +using Cysharp.Threading.Tasks; +using DCL.AssetsProvision; +using DCL.Audio; +using DCL.AvatarRendering.Emotes; +using DCL.AvatarRendering.Emotes.Equipped; +using DCL.AvatarRendering.Wearables; +using DCL.AvatarRendering.Wearables.Equipped; +using DCL.AvatarRendering.Wearables.Helpers; +using DCL.AvatarRendering.Wearables.ThirdParty; +using DCL.Backpack; +using DCL.Backpack.BackpackBus; +using DCL.Backpack.Gifting.Services.PendingTransfers; +using DCL.Browser; +using DCL.CharacterPreview; +using DCL.Chat.EventBus; +using DCL.ExplorePanel; +using DCL.Input; +using DCL.Landscape.Settings; +using DCL.MapRenderer; +using DCL.Navmap; +using DCL.PlacesAPIService; +using DCL.Profiles; +using DCL.Profiles.Self; +using DCL.Quality; +using DCL.Settings; +using DCL.Settings.Configuration; +using DCL.UI.ProfileElements; +using DCL.UserInAppInitializationFlow; +using DCL.Web3.Authenticators; +using DCL.Web3.Identities; +using DCL.WebRequests; +using ECS; +using ECS.Prioritization; +using Global.Dynamic; +using MVC; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using DCL.Backpack.AvatarSection.Outfits.Repository; +using DCL.Chat.MessageBus; +using DCL.Clipboard; +using DCL.Communities; +using DCL.Communities.CommunitiesBrowser; +using DCL.Communities.CommunitiesDataProvider; +using DCL.Donations; +using DCL.EventsApi; +using DCL.FeatureFlags; +using DCL.Friends.UserBlocking; +using DCL.InWorldCamera; +using DCL.Navmap.ScriptableObjects; +using DCL.InWorldCamera.CameraReelGallery; +using DCL.InWorldCamera.CameraReelGallery.Components; +using DCL.InWorldCamera.CameraReelStorageService; +using DCL.MapRenderer.MapLayers.HomeMarker; +using DCL.Multiplayer.Connections.DecentralandUrls; +using DCL.Optimization.PerformanceBudgeting; +using DCL.Passport; +using DCL.PerformanceAndDiagnostics.Analytics; +using DCL.Places; +using DCL.RealmNavigation; +using DCL.UI.Profiles.Helpers; +using DCL.SDKComponents.MediaStream.Settings; +using DCL.Settings.Settings; +using DCL.SkyBox; +using DCL.UI; +using DCL.UI.Profiles; +using DCL.UI.SharedSpaceManager; +using DCL.Utilities; +using Utility; +using DCL.VoiceChat; +using ECS.SceneLifeCycle.IncreasingRadius; +using ECS.SceneLifeCycle.Realm; +using Global.AppArgs; +using Runtime.Wearables; +using UnityEngine; +using UnityEngine.AddressableAssets; +using UnityEngine.Audio; +using UnityEngine.Pool; +using Object = UnityEngine.Object; + +// ReSharper disable UnusedAutoPropertyAccessor.Local +namespace DCL.PluginSystem.Global +{ + public class ExplorePanelPlugin : IDCLGlobalPlugin + { + private readonly IEventBus eventBus; + private readonly FeatureFlagsConfiguration featureFlags; + private readonly IAssetsProvisioner assetsProvisioner; + private readonly MapRendererContainer mapRendererContainer; + private readonly IMVCManager mvcManager; + private readonly IPlacesAPIService placesAPIService; + private readonly IProfileRepository profileRepository; + private readonly IUserInAppInitializationFlow userInAppInitializationFlow; + private readonly ISelfProfile selfProfile; + private readonly IEquippedWearables equippedWearables; + private readonly IEquippedEmotes equippedEmotes; + private readonly IWeb3Authenticator web3Authenticator; + private readonly IWeb3IdentityCache web3IdentityCache; + private readonly ICameraReelStorageService cameraReelStorageService; + private readonly ICameraReelScreenshotsStorage cameraReelScreenshotsStorage; + private readonly ISystemClipboard systemClipboard; + private readonly IDecentralandUrlsSource decentralandUrlsSource; + private readonly IWearableStorage wearableStorage; + private readonly ICharacterPreviewFactory characterPreviewFactory; + private readonly IWebBrowser webBrowser; + private readonly IEmoteStorage emoteStorage; + private readonly IWebRequestController webRequestController; + private readonly CharacterPreviewEventBus characterPreviewEventBus; + private readonly IBackpackEventBus backpackEventBus; + private readonly IThirdPartyNftProviderSource thirdPartyNftProviderSource; + private readonly IWearablesProvider wearablesProvider; + private readonly ICursor cursor; + private readonly IEmoteProvider emoteProvider; + private readonly Arch.Core.World world; + private readonly Entity playerEntity; + private readonly IMapPathEventBus mapPathEventBus; + private readonly IRealmData realmData; + private readonly IProfileCache profileCache; + private readonly URLDomain assetBundleURL; + private readonly IInputBlock inputBlock; + private readonly IChatMessagesBus chatMessagesBus; + private readonly ISystemMemoryCap systemMemoryCap; + private readonly VolumeBus volumeBus; + private readonly HttpEventsApiService eventsApiService; + private readonly GoogleUserCalendar userCalendar; + private readonly ISystemClipboard clipboard; + private readonly ObjectProxy explorePanelNavmapBus; + private readonly IAppArgs appArgs; + private readonly ObjectProxy userBlockingCacheProxy; + private readonly ISharedSpaceManager sharedSpaceManager; + private readonly SceneLoadingLimit sceneLoadingLimit; + private readonly WarningNotificationView inWorldWarningNotificationView; + private readonly ProfileChangesBus profileChangesBus; + private readonly CommunitiesDataProvider communitiesDataProvider; + private readonly INftNamesProvider nftNamesProvider; + private readonly IThumbnailProvider thumbnailProvider; + private readonly IChatEventBus chatEventBus; + private readonly HomePlaceEventBus homePlaceEventBus; + private readonly IPendingTransferService pendingTransferService; + + private readonly bool includeCameraReel; + private readonly bool includeDiscover; + + private NavmapController? navmapController; + private SettingsController? settingsController; + private BackpackSubPlugin? backpackSubPlugin; + private CategoryFilterController? categoryFilterController; + private SearchResultPanelController? searchResultPanelController; + private PlacesAndEventsPanelController? placesAndEventsPanelController; + private NavmapView? navmapView; + private PlaceInfoPanelController? placeInfoPanelController; + private NavmapSearchBarController? searchBarController; + private EventInfoPanelController? eventInfoPanelController; + private readonly ProfileRepositoryWrapper profileRepositoryWrapper; + private readonly UpscalingController upscalingController; + private CommunitiesBrowserController? communitiesBrowserController; + private PlacesController? placesController; + private readonly bool isVoiceChatEnabled; + private readonly bool isTranslationChatEnabled; + private readonly GalleryEventBus galleryEventBus; + private readonly IVoiceChatOrchestrator communityCallOrchestrator; + private readonly IPassportBridge passportBridge; + private readonly SmartWearableCache smartWearableCache; + private readonly IAnalyticsController analytics; + private readonly CommunityDataService communityDataService; + private readonly ILoadingStatus loadingStatus; + private readonly ImageControllerProvider imageControllerProvider; + private readonly IDonationsService donationsService; + private readonly IRealmNavigator realmNavigator; + + public ExplorePanelPlugin(IEventBus eventBus, + FeatureFlagsConfiguration featureFlags, + IAssetsProvisioner assetsProvisioner, + IMVCManager mvcManager, + MapRendererContainer mapRendererContainer, + IPlacesAPIService placesAPIService, + IWebRequestController webRequestController, + IWeb3IdentityCache web3IdentityCache, + ICameraReelStorageService cameraReelStorageService, + ICameraReelScreenshotsStorage cameraReelScreenshotsStorage, + ISystemClipboard systemClipboard, + IDecentralandUrlsSource decentralandUrlsSource, + IWearableStorage wearableStorage, + ICharacterPreviewFactory characterPreviewFactory, + IProfileRepository profileRepository, + IWeb3Authenticator web3Authenticator, + IUserInAppInitializationFlow userInAppInitializationFlow, + ISelfProfile selfProfile, + IEquippedWearables equippedWearables, + IEquippedEmotes equippedEmotes, + IWebBrowser webBrowser, + IEmoteStorage emoteStorage, + IRealmData realmData, + IProfileCache profileCache, + CharacterPreviewEventBus characterPreviewEventBus, + IMapPathEventBus mapPathEventBus, + IBackpackEventBus backpackEventBus, + IThirdPartyNftProviderSource thirdPartyNftProviderSource, + IWearablesProvider wearablesProvider, + ICursor cursor, + IInputBlock inputBlock, + IEmoteProvider emoteProvider, + Arch.Core.World world, + Entity playerEntity, + IChatMessagesBus chatMessagesBus, + ISystemMemoryCap systemMemoryCap, + VolumeBus volumeBus, + HttpEventsApiService eventsApiService, + GoogleUserCalendar userCalendar, + ISystemClipboard clipboard, + ObjectProxy explorePanelNavmapBus, + bool includeCameraReel, + bool includeDiscover, + IAppArgs appArgs, + ObjectProxy userBlockingCacheProxy, + ISharedSpaceManager sharedSpaceManager, + ProfileChangesBus profileChangesBus, + SceneLoadingLimit sceneLoadingLimit, + WarningNotificationView inWorldWarningNotificationView, + ProfileRepositoryWrapper profileDataProvider, + UpscalingController upscalingController, + CommunitiesDataProvider communitiesDataProvider, + INftNamesProvider nftNamesProvider, + IVoiceChatOrchestrator communityCallOrchestrator, + bool isTranslationChatEnabled, + GalleryEventBus galleryEventBus, + IThumbnailProvider thumbnailProvider, + IPassportBridge passportBridge, + IChatEventBus chatEventBus, + HomePlaceEventBus homePlaceEventBus, + SmartWearableCache smartWearableCache, + ImageControllerProvider imageControllerProvider, + IAnalyticsController analytics, + CommunityDataService communityDataService, + ILoadingStatus loadingStatus, + IPendingTransferService pendingTransferService, + IDonationsService donationsService, + IRealmNavigator realmNavigator) + { + this.eventBus = eventBus; + this.featureFlags = featureFlags; + this.assetsProvisioner = assetsProvisioner; + this.mvcManager = mvcManager; + this.mapRendererContainer = mapRendererContainer; + this.placesAPIService = placesAPIService; + this.webRequestController = webRequestController; + this.web3IdentityCache = web3IdentityCache; + this.cameraReelStorageService = cameraReelStorageService; + this.cameraReelScreenshotsStorage = cameraReelScreenshotsStorage; + this.systemClipboard = systemClipboard; + this.decentralandUrlsSource = decentralandUrlsSource; + this.wearableStorage = wearableStorage; + this.characterPreviewFactory = characterPreviewFactory; + this.profileRepository = profileRepository; + this.web3Authenticator = web3Authenticator; + this.userInAppInitializationFlow = userInAppInitializationFlow; + this.selfProfile = selfProfile; + this.equippedWearables = equippedWearables; + this.equippedEmotes = equippedEmotes; + this.webBrowser = webBrowser; + this.realmData = realmData; + this.profileCache = profileCache; + this.emoteStorage = emoteStorage; + this.characterPreviewEventBus = characterPreviewEventBus; + this.mapPathEventBus = mapPathEventBus; + this.backpackEventBus = backpackEventBus; + this.thirdPartyNftProviderSource = thirdPartyNftProviderSource; + this.wearablesProvider = wearablesProvider; + this.inputBlock = inputBlock; + this.cursor = cursor; + this.emoteProvider = emoteProvider; + this.world = world; + this.playerEntity = playerEntity; + this.chatMessagesBus = chatMessagesBus; + this.systemMemoryCap = systemMemoryCap; + this.volumeBus = volumeBus; + this.eventsApiService = eventsApiService; + this.userCalendar = userCalendar; + this.clipboard = clipboard; + this.explorePanelNavmapBus = explorePanelNavmapBus; + this.includeCameraReel = includeCameraReel; + this.includeDiscover = includeDiscover; + this.appArgs = appArgs; + this.userBlockingCacheProxy = userBlockingCacheProxy; + this.sharedSpaceManager = sharedSpaceManager; + this.profileChangesBus = profileChangesBus; + this.sceneLoadingLimit = sceneLoadingLimit; + this.inWorldWarningNotificationView = inWorldWarningNotificationView; + this.profileRepositoryWrapper = profileDataProvider; + this.upscalingController = upscalingController; + this.communitiesDataProvider = communitiesDataProvider; + this.nftNamesProvider = nftNamesProvider; + this.isTranslationChatEnabled = isTranslationChatEnabled; + this.galleryEventBus = galleryEventBus; + this.communityCallOrchestrator = communityCallOrchestrator; + this.thumbnailProvider = thumbnailProvider; + this.chatEventBus = chatEventBus; + this.homePlaceEventBus = homePlaceEventBus; + this.passportBridge = passportBridge; + this.smartWearableCache = smartWearableCache; + this.imageControllerProvider = imageControllerProvider; + this.analytics = analytics; + this.communityDataService = communityDataService; + this.loadingStatus = loadingStatus; + this.donationsService = donationsService; + this.realmNavigator = realmNavigator; + this.pendingTransferService = pendingTransferService; + } + + public void Dispose() + { + categoryFilterController?.Dispose(); + navmapController?.Dispose(); + settingsController?.Dispose(); + backpackSubPlugin?.Dispose(); + placeInfoPanelController?.Dispose(); + communitiesBrowserController?.Dispose(); + placesController?.Dispose(); + upscalingController?.Dispose(); + } + + public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in GlobalPluginArguments arguments) { } + + public async UniTask InitializeAsync(ExplorePanelSettings settings, CancellationToken ct) + { + INavmapBus navmapBus = new NavmapCommandBus(CreateSearchPlaceCommand, + CreateShowPlaceCommand, CreateShowEventCommand, placesAPIService); + + explorePanelNavmapBus.SetObject(navmapBus); + + var outfitsRepository = new OutfitsRepository(realmData, nftNamesProvider); + + backpackSubPlugin = new BackpackSubPlugin( + featureFlags, + assetsProvisioner, + web3IdentityCache, + characterPreviewFactory, + wearableStorage, + selfProfile, + profileCache, + equippedWearables, + equippedEmotes, + emoteStorage, + characterPreviewEventBus, + backpackEventBus, + thirdPartyNftProviderSource, + wearablesProvider, + inputBlock, + cursor, + emoteProvider, + world, + playerEntity, + appArgs, + webBrowser, + inWorldWarningNotificationView, + thumbnailProvider, + profileChangesBus, + outfitsRepository, + realmData, + webRequestController, + nftNamesProvider, + eventBus, + smartWearableCache, + mvcManager, + decentralandUrlsSource, + pendingTransferService + ); + + ExplorePanelView panelViewAsset = (await assetsProvisioner.ProvideMainAssetValueAsync(settings.ExplorePanelPrefab, ct: ct)).GetComponent(); + ControllerBase.ViewFactoryMethod viewFactoryMethod = ExplorePanelController.Preallocate(panelViewAsset, null, out ExplorePanelView explorePanelView); + + ProvidedAsset generalAudioMixer = await assetsProvisioner.ProvideMainAssetAsync(settings.GeneralAudioMixer, ct); + + ProvidedAsset landscapeData = await assetsProvisioner.ProvideMainAssetAsync(settings.LandscapeData, ct); + + ProvidedAsset categoryMappingSO = await assetsProvisioner.ProvideMainAssetAsync(settings.CategoryMappingSO, ct); + + ProvidedAsset placeCategoriesSO = await assetsProvisioner.ProvideMainAssetAsync(settings.PlaceCategoriesSO, ct); + + navmapView = explorePanelView.GetComponentInChildren(); + categoryFilterController = new CategoryFilterController(navmapView.categoryToggles, mapRendererContainer.MapRenderer, navmapBus); + + NavmapZoomController zoomController = new (navmapView.zoomView, navmapBus); + + ObjectPool placeElementsPool = await InitializePlaceElementsPoolAsync(navmapView.SearchBarResultPanel, ct); + ObjectPool eventElementsPool = await InitializeEventElementsForPlacePoolAsync(navmapView.PlacesAndEventsPanelView.PlaceInfoPanelView, ct); + ObjectPool eventScheduleElementsPool = await InitializeEventScheduleElementsPoolAsync(navmapView.PlacesAndEventsPanelView.EventInfoPanelView, ct); + + searchResultPanelController = new SearchResultPanelController(navmapView.SearchBarResultPanel, + placeElementsPool, navmapBus); + + searchBarController = new NavmapSearchBarController(navmapView.SearchBarView, + navmapView.HistoryRecordPanelView, navmapView.PlacesAndEventsPanelView.SearchFiltersView, + inputBlock, navmapBus, categoryMappingSO.Value); + + SharePlacesAndEventsContextMenuController shareContextMenu = new (navmapView.ShareContextMenuView, + navmapView.WorldsWarningNotificationView, clipboard, webBrowser); + + placeInfoPanelController = new PlaceInfoPanelController(navmapView.PlacesAndEventsPanelView.PlaceInfoPanelView, + imageControllerProvider, placesAPIService, mapPathEventBus, navmapBus, chatMessagesBus, eventsApiService, + eventElementsPool, shareContextMenu, webBrowser, mvcManager, homePlaceEventBus, donationsService, cameraReelStorageService, cameraReelScreenshotsStorage, + new ReelGalleryConfigParams( + settings.PlaceGridLayoutFixedColumnCount, + settings.PlaceThumbnailHeight, + settings.PlaceThumbnailWidth, + false, + false), + false, + galleryEventBus: galleryEventBus); + + eventInfoPanelController = new EventInfoPanelController(navmapView.PlacesAndEventsPanelView.EventInfoPanelView, + navmapBus, chatMessagesBus, eventsApiService, eventScheduleElementsPool, + userCalendar, shareContextMenu, webBrowser, imageControllerProvider); + + placesAndEventsPanelController = new PlacesAndEventsPanelController(navmapView.PlacesAndEventsPanelView, + searchBarController, searchResultPanelController, placeInfoPanelController, eventInfoPanelController, + zoomController); + + IMapRenderer mapRenderer = mapRendererContainer.MapRenderer; + + SatelliteController satelliteController = new (navmapView.GetComponentInChildren(), + navmapView.MapCameraDragBehaviorData, mapRenderer, webBrowser); + + PlaceInfoToastController placeToastController = new (navmapView.PlaceToastView, + new PlaceInfoPanelController(navmapView.PlaceToastView.PlacePanelView, + imageControllerProvider, placesAPIService, mapPathEventBus, navmapBus, chatMessagesBus, eventsApiService, + eventElementsPool, shareContextMenu, webBrowser, mvcManager, homePlaceEventBus, donationsService, galleryEventBus: galleryEventBus), + placesAPIService, eventsApiService, navmapBus); + + settingsController = new SettingsController( + explorePanelView.GetComponentInChildren(), + settings.SettingsMenuConfiguration, + generalAudioMixer.Value, + settings.RealmPartitionSettings, + settings.VideoPrioritizationSettings, + landscapeData.Value, + settings.QualitySettingsAsset, + settings.SkyboxSettingsAsset, + settings.ControlsSettingsAsset, + systemMemoryCap, + settings.ChatSettingsAsset, + userBlockingCacheProxy, + sceneLoadingLimit, + volumeBus, + upscalingController, + isTranslationChatEnabled, + assetsProvisioner, + eventBus, + appArgs); + + await settingsController.InitializeAsync(); + + navmapController = new NavmapController( + navmapView: explorePanelView.GetComponentInChildren(), + mapRendererContainer.MapRenderer, + realmData, + mapPathEventBus, + world, + playerEntity, + navmapBus, + UIAudioEventsBus.Instance, + placesAndEventsPanelController, + searchBarController, + zoomController, + satelliteController, + placesAPIService, + homePlaceEventBus); + + await backpackSubPlugin.InitializeAsync(settings.BackpackSettings, explorePanelView.GetComponentInChildren(), ct); + + CameraReelView cameraReelView = explorePanelView.GetComponentInChildren(); + + var cameraReelController = new CameraReelController(cameraReelView, + new CameraReelGalleryController( + cameraReelView.CameraReelGalleryView, + this.cameraReelStorageService, + cameraReelScreenshotsStorage, + new ReelGalleryConfigParams(settings.GridLayoutFixedColumnCount, settings.ThumbnailHeight, settings.ThumbnailWidth, true, true), true, + galleryEventBus, + cameraReelView.CameraReelOptionsButton, + webBrowser, decentralandUrlsSource, systemClipboard, + settings.CameraReelGalleryMessages, + mvcManager), + cameraReelStorageService, + web3IdentityCache, + mvcManager, + cursor, + galleryEventBus, + settings.StorageProgressBarText); + + CommunitiesBrowserView communitiesBrowserView = explorePanelView.GetComponentInChildren(); + + communitiesBrowserController = new CommunitiesBrowserController( + communitiesBrowserView, + cursor, + communitiesDataProvider, + webRequestController, + inputBlock, + mvcManager, + profileRepositoryWrapper, + selfProfile, + nftNamesProvider, + communityCallOrchestrator, + sharedSpaceManager, + chatEventBus, + analytics, + communityDataService, + loadingStatus); + + PlacesView placesView = explorePanelView.GetComponentInChildren(); + placesController = new PlacesController(placesView, cursor, placesAPIService, placeCategoriesSO.Value, inputBlock, selfProfile, webBrowser, webRequestController, realmNavigator, clipboard, decentralandUrlsSource); + + ExplorePanelController explorePanelController = new + ExplorePanelController(viewFactoryMethod, + navmapController, + settingsController, + backpackSubPlugin.backpackController!, + cameraReelController, + new ProfileWidgetController(() => explorePanelView.ProfileWidget, + web3IdentityCache, + profileRepository, + profileChangesBus), + new ProfileMenuController(() => explorePanelView.ProfileMenuView, + web3IdentityCache, + profileRepository, + world, + playerEntity, + webBrowser, + web3Authenticator, + userInAppInitializationFlow, + profileCache, + passportBridge, + profileRepositoryWrapper), + communitiesBrowserController, + placesController, + inputBlock, + includeCameraReel, + includeDiscover, + sharedSpaceManager); + + sharedSpaceManager.RegisterPanel(PanelsSharingSpace.Explore, explorePanelController); + mvcManager.RegisterController(explorePanelController); + } + + private async UniTask> InitializePlaceElementsPoolAsync(SearchResultPanelView view, CancellationToken ct) + { + PlaceElementView asset = (await assetsProvisioner.ProvideInstanceAsync(view.ResultRef, ct: ct)).Value; + + return new ObjectPool( + () => CreatePoolElements(asset), + actionOnGet: result => result.gameObject.SetActive(true), + actionOnRelease: result => result.gameObject.SetActive(false), + defaultCapacity: 8 + ); + + PlaceElementView CreatePoolElements(PlaceElementView asset) + { + PlaceElementView placeElementView = Object.Instantiate(asset, view.searchResultsContainer); + placeElementView.ConfigurePlaceImageController(imageControllerProvider); + return placeElementView; + } + } + + private async UniTask> InitializeEventElementsForPlacePoolAsync(PlaceInfoPanelView view, CancellationToken ct) + { + EventElementView asset = (await assetsProvisioner.ProvideInstanceAsync(view.EventElementViewRef, ct: ct)).Value; + + return new ObjectPool( + () => CreatePoolElements(asset), + actionOnGet: result => result.gameObject.SetActive(true), + actionOnRelease: result => result.gameObject.SetActive(false), + defaultCapacity: 8 + ); + + EventElementView CreatePoolElements(EventElementView asset) + { + EventElementView placeElementView = Object.Instantiate(asset, view.EventsContentContainer.transform); + return placeElementView; + } + } + + private async UniTask> InitializeEventScheduleElementsPoolAsync(EventInfoPanelView view, CancellationToken ct) + { + EventScheduleElementView asset = (await assetsProvisioner.ProvideInstanceAsync(view.ScheduleElementRef, ct: ct)).Value; + + return new ObjectPool( + () => CreatePoolElements(asset), + actionOnGet: result => result.gameObject.SetActive(true), + actionOnRelease: result => result.gameObject.SetActive(false), + defaultCapacity: 8 + ); + + EventScheduleElementView CreatePoolElements(EventScheduleElementView asset) + { + EventScheduleElementView placeElementView = Object.Instantiate(asset, view.ScheduleElementsContainer); + return placeElementView; + } + } + + private INavmapCommand CreateSearchPlaceCommand(INavmapBus.SearchPlaceResultDelegate callback, INavmapBus.SearchPlaceParams @params) => + new SearchForPlaceAndShowResultsCommand(placesAPIService, eventsApiService, placesAndEventsPanelController!, + searchResultPanelController!, searchBarController!, callback, + @params); + + private INavmapCommand CreateShowPlaceCommand(PlacesData.PlaceInfo placeInfo) => + new ShowPlaceInfoCommand(placeInfo, placeInfoPanelController!, placesAndEventsPanelController!, eventsApiService, + searchBarController!); + + private INavmapCommand CreateShowEventCommand(EventDTO @event, PlacesData.PlaceInfo? place = null) => + new ShowEventInfoCommand(@event, eventInfoPanelController!, placesAndEventsPanelController!, + searchBarController!, placesAPIService, place); + + public class ExplorePanelSettings : IDCLPluginSettings + { + [field: Header(nameof(ExplorePanelPlugin) + "." + nameof(ExplorePanelSettings))] + [field: Space] + [field: SerializeField] + public AssetReferenceGameObject ExplorePanelPrefab; + + [field: SerializeField] + public BackpackSettings BackpackSettings { get; private set; } + + [field: SerializeField] + public SettingsMenuConfiguration SettingsMenuConfiguration { get; private set; } + + [field: SerializeField] + public AssetReferenceT GeneralAudioMixer { get; private set; } + + [field: SerializeField] + public RealmPartitionSettingsAsset RealmPartitionSettings { get; private set; } + + [field: SerializeField] + public VideoPrioritizationSettings VideoPrioritizationSettings { get; private set; } + + [field: SerializeField] + public LandscapeDataRef LandscapeData { get; private set; } + + [field: SerializeField] + public QualitySettingsAsset QualitySettingsAsset { get; private set; } + [field: SerializeField] + public SkyboxSettingsAsset SkyboxSettingsAsset { get; private set; } + + [field: SerializeField] + public ControlsSettingsAsset ControlsSettingsAsset { get; private set; } + + [field: SerializeField] + public ChatSettingsAsset ChatSettingsAsset { get; private set; } + + [field: SerializeField] + public AssetReferenceT CategoryMappingSO { get; private set; } + + [field: Header("Camera Reel")] + [field: SerializeField] + [field: Tooltip("Spaces will be HTTP sanitized, care for special characters")] + public CameraReelGalleryMessagesConfiguration CameraReelGalleryMessages { get; private set; } + + [field: SerializeField] + public string StorageProgressBarText { get; private set; } + + [field: SerializeField] + public int GridLayoutFixedColumnCount { get; private set; } + [field: SerializeField] + public int ThumbnailHeight { get; private set; } + [field: SerializeField] + public int ThumbnailWidth { get; private set; } + + [field: Header("Place Reel")] + [field: SerializeField] + public int PlaceGridLayoutFixedColumnCount { get; private set; } + + [field: SerializeField] + public int PlaceThumbnailHeight { get; private set; } + + [field: SerializeField] + public int PlaceThumbnailWidth { get; private set; } + + [field: SerializeField] + public AssetReferenceT PlaceCategoriesSO { get; private set; } + } + } +} diff --git a/Explorer/Assets/DCL/PluginSystem/Global/GiftingPlugin.cs b/Explorer/Assets/DCL/PluginSystem/Global/GiftingPlugin.cs index 6d9353b5aa..0ef16068f5 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/GiftingPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/Global/GiftingPlugin.cs @@ -194,7 +194,6 @@ public async UniTask InitializeAsync(GiftingSettings settings, CancellationToken componentFactory, giftInventoryService, equippedStatusProvider, - profileRepository, mvcManager ); diff --git a/Explorer/Assets/DCL/Prefs/DCLPrefKeys.cs b/Explorer/Assets/DCL/Prefs/DCLPrefKeys.cs index c30f0368d3..f458735186 100644 --- a/Explorer/Assets/DCL/Prefs/DCLPrefKeys.cs +++ b/Explorer/Assets/DCL/Prefs/DCLPrefKeys.cs @@ -70,7 +70,9 @@ public static class DCLPrefKeys public const string MAP_HOME_MARKER_DATA = "Map_HomeMarker"; - public const string GIFTING_PENDING_GIFTS = "PendingGifts"; + public const string GIFTING_PENDING_GIFTS = "PendingGifts"; // Legacy, kept for migration + public const string GIFTING_PENDING_WEARABLES_V2 = "PendingGifts_Wearables_v2"; + public const string GIFTING_PENDING_EMOTES_V2 = "PendingGifts_Emotes_v2"; public const string SETTINGS_HEAD_SYNC_ENABLED = "Settings_HeadSync"; diff --git a/Explorer/Assets/DCL/UI/GenericContextMenu/Controllers/GenericUserProfileContextMenuController.cs b/Explorer/Assets/DCL/UI/GenericContextMenu/Controllers/GenericUserProfileContextMenuController.cs index 717a20d110..71eae18cd2 100644 --- a/Explorer/Assets/DCL/UI/GenericContextMenu/Controllers/GenericUserProfileContextMenuController.cs +++ b/Explorer/Assets/DCL/UI/GenericContextMenu/Controllers/GenericUserProfileContextMenuController.cs @@ -40,11 +40,13 @@ public struct GiftData { public string userId; public string userName; + public Color usernameColor; - public GiftData(string userId, string userName) + public GiftData(string userId, string userName, Color usernameColor) { this.userId = userId; this.userName = userName; + this.usernameColor = usernameColor; } } public class GenericUserProfileContextMenuController @@ -186,7 +188,7 @@ public async UniTask ShowUserProfileContextMenuAsync(Profile.CompactInfo profile blockButtonControlSettings.SetData(profile.UserId); jumpInButtonControlSettings.SetData(profile.UserId); - string? json = JsonUtility.ToJson(new GiftData(profile.UserId, profile.DisplayName)); + string? json = JsonUtility.ToJson(new GiftData(profile.UserId, profile.DisplayName,profile.UserNameColor)); giftButtonControlSettings.SetData(json); contextMenuBlockUserButton.Enabled = includeUserBlocking && friendshipStatus != FriendshipStatus.BLOCKED; @@ -389,7 +391,7 @@ private void OnGiftUserClicked(string payload) try { var data = JsonUtility.FromJson(payload); - ShowGiftingPopupAsync(data.userId, data.userName).Forget(); + ShowGiftingPopupAsync(data.userId, data.userName,data.usernameColor).Forget(); } catch { @@ -398,9 +400,9 @@ private void OnGiftUserClicked(string payload) } } - private async UniTaskVoid ShowGiftingPopupAsync(string userId, string userName) + private async UniTaskVoid ShowGiftingPopupAsync(string userId, string userName, Color color) { - await mvcManager.ShowAsync(GiftSelectionController.IssueCommand(new GiftSelectionParams(userId, userName))); + await mvcManager.ShowAsync(GiftSelectionController.IssueCommand(new GiftSelectionParams(userId, userName,color))); } } } diff --git a/Explorer/Assets/Locales/Localization Tables/UI Text Localization Table Shared Data.asset b/Explorer/Assets/Locales/Localization Tables/UI Text Localization Table Shared Data.asset index 81c324f514..e1c2aaac32 100644 --- a/Explorer/Assets/Locales/Localization Tables/UI Text Localization Table Shared Data.asset +++ b/Explorer/Assets/Locales/Localization Tables/UI Text Localization Table Shared Data.asset @@ -45,6 +45,10 @@ MonoBehaviour: m_Key: Places m_Metadata: m_Items: [] + - m_Id: 299644359562919936 + m_Key: PENDING_WEARABLE_TRANSFER + m_Metadata: + m_Items: [] m_Metadata: m_Items: [] m_KeyGenerator: diff --git a/Explorer/Assets/Locales/Localization Tables/UI Text Localization Table_en.asset b/Explorer/Assets/Locales/Localization Tables/UI Text Localization Table_en.asset index 642453373f..a08c86538c 100644 --- a/Explorer/Assets/Locales/Localization Tables/UI Text Localization Table_en.asset +++ b/Explorer/Assets/Locales/Localization Tables/UI Text Localization Table_en.asset @@ -48,6 +48,10 @@ MonoBehaviour: m_Localized: Places m_Metadata: m_Items: [] + - m_Id: 299644359562919936 + m_Localized: The wearable is being transferred. + m_Metadata: + m_Items: [] references: version: 2 RefIds: