Skip to content

Commit

Permalink
feat(wallet-dashboard): Improve assets filter (#4512)
Browse files Browse the repository at this point in the history
* refactor(wallet): move notification to separate component.

* feat(wallet): enhance hidden asset functionality with undo option and notifications

* feat(wallet): add undo functionality for showing hidden assets with notifications

* feat(core): move HiddenAssetsProvider.

* feat(core): move useGetNFTs.

* feat(core): update useGetNFTs to accept filter and improve asset fetching

* refactor(dashboard): remove 'Hidden' asset category and related logic

* refactor(wallet-dashboard): remove 'Hidden' asset category from layout

* refactor(dashboard): remove HiddenAssets context, update default select asset logic.

* feat(core): add refetch capability to useGetNFTs hook

* refactor(core): improve hide/show logic

* refactor(core): remove undo functionality and streamline asset visibility management

* refactor(core): simplify asset visibility management and improve error handling

* refactor(wallet): rename MoveAssetNotification to MovedAssetNotification

* refactor(dashboard): adapt logic to useGetNFTs hook.

* fix(wallet-dashboard): improve asset loading logic and conditional rendering

* feat(wallet-dashboard): move logic for page to the hook

* fix(dashboard): load more if we have intersection

* feat(ui-kit, dashboard): add disabled state to Chip component and update assets logic

* refactor(core): reorganize usePageAssets hook into ui directory

* refactor(core): move filter from param to hook direct.
  • Loading branch information
panteleymonchuk authored and piotrm50 committed Jan 15, 2025
1 parent 4f02c12 commit 55b6b80
Show file tree
Hide file tree
Showing 25 changed files with 512 additions and 454 deletions.
1 change: 1 addition & 0 deletions apps/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"bignumber.js": "^9.1.1",
"clsx": "^2.1.1",
"formik": "^2.4.2",
"idb-keyval": "^6.2.1",
"qrcode.react": "^4.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
112 changes: 112 additions & 0 deletions apps/core/src/contexts/HiddenAssetsProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Copyright (c) Mysten Labs, Inc.
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { get, set } from 'idb-keyval';
import {
PropsWithChildren,
createContext,
useCallback,
useContext,
useEffect,
useState,
useRef,
} from 'react';

const HIDDEN_ASSET_IDS = 'hidden-asset-ids';

export type HiddenAssets =
| {
type: 'loading';
}
| {
type: 'loaded';
assetIds: string[];
};

interface HiddenAssetContext {
hiddenAssets: HiddenAssets;
hideAsset: (assetId: string) => string | void;
showAsset: (assetId: string) => string | void;
}

export const HiddenAssetsContext = createContext<HiddenAssetContext>({
hiddenAssets: {
type: 'loading',
},
hideAsset: () => {},
showAsset: () => {},
});

export const HiddenAssetsProvider = ({ children }: PropsWithChildren) => {
const [hiddenAssets, setHiddenAssets] = useState<HiddenAssets>({
type: 'loading',
});
const hiddenAssetIdsRef = useRef<string[]>([]);

useEffect(() => {
(async () => {
try {
const hiddenAssetsFromStorage = (await get<string[]>(HIDDEN_ASSET_IDS)) ?? [];
hiddenAssetIdsRef.current = hiddenAssetsFromStorage;
setHiddenAssetIds(hiddenAssetsFromStorage);
} catch (error) {
console.error('Failed to load hidden assets from storage:', error);
setHiddenAssetIds([]);
}
})();
}, []);

function setHiddenAssetIds(hiddenAssetIds: string[]) {
hiddenAssetIdsRef.current = hiddenAssetIds;
setHiddenAssets({
type: 'loaded',
assetIds: hiddenAssetIds,
});
}

const syncIdb = useCallback(async (nextState: string[], prevState: string[]) => {
try {
await set(HIDDEN_ASSET_IDS, nextState);
} catch (error) {
console.error('Error syncing with IndexedDB:', error);
// Revert to the previous state on failure
setHiddenAssetIds(prevState);
}
}, []);

const hideAsset = useCallback((assetId: string) => {
const prevIds = [...hiddenAssetIdsRef.current];
const newHiddenAssetIds = Array.from(new Set([...hiddenAssetIdsRef.current, assetId]));
setHiddenAssetIds(newHiddenAssetIds);
syncIdb(newHiddenAssetIds, prevIds);
return assetId;
}, []);

const showAsset = useCallback((assetId: string) => {
// Ensure the asset exists in the hidden list
if (!hiddenAssetIdsRef.current.includes(assetId)) return;

const prevIds = [...hiddenAssetIdsRef.current];
// Compute the new list of hidden assets
const updatedHiddenAssetIds = hiddenAssetIdsRef.current.filter((id) => id !== assetId);
setHiddenAssetIds(updatedHiddenAssetIds);
syncIdb(updatedHiddenAssetIds, prevIds);
}, []);

return (
<HiddenAssetsContext.Provider
value={{
hiddenAssets,
hideAsset,
showAsset,
}}
>
{children}
</HiddenAssetsContext.Provider>
);
};

export const useHiddenAssets = () => {
return useContext(HiddenAssetsContext);
};
1 change: 1 addition & 0 deletions apps/core/src/contexts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
// SPDX-License-Identifier: Apache-2.0

export * from './ThemeContext';
export * from './HiddenAssetsProvider';
2 changes: 2 additions & 0 deletions apps/core/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export * from './useOwnedNFT';
export * from './useNftDetails';
export * from './useCountdownByTimestamp';
export * from './useStakeRewardStatus';
export * from './useGetNFTs';
export * from './useRecognizedPackages';

export * from './stake';
export * from './ui';
4 changes: 4 additions & 0 deletions apps/core/src/hooks/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

export * from './usePageAssets';
131 changes: 131 additions & 0 deletions apps/core/src/hooks/ui/usePageAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
import { useState, useMemo, useRef, useEffect } from 'react';
import { useGetNFTs, HiddenAssets, useOnScreen } from '../..';

export enum AssetCategory {
Visual = 'Visual',
Other = 'Other',
Hidden = 'Hidden',
}

export function usePageAssets(address: string | null, hiddenAssets?: HiddenAssets) {
const [selectedAssetCategory, setSelectedAssetCategory] = useState<AssetCategory | null>(null);
const observerElem = useRef<HTMLDivElement | null>(null);
const { isIntersecting } = useOnScreen(observerElem);
const {
data: ownedAssets,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
error,
isPending,
isError,
isFetching,
refetch,
} = useGetNFTs(address, hiddenAssets);

const isAssetsLoaded = !!ownedAssets;
const isSpinnerVisible = isFetchingNextPage && hasNextPage;

const filteredAssets = (() => {
if (!ownedAssets) return [];
switch (selectedAssetCategory) {
case AssetCategory.Visual:
return ownedAssets.visual;
case AssetCategory.Other:
return ownedAssets.other;
default:
return [];
}
})();

const filteredHiddenAssets = useMemo(() => {
return (
ownedAssets?.hidden
.flatMap((data) => {
return {
data: data,
display: data?.display?.data,
};
})
.sort((nftA, nftB) => {
const nameA = nftA.display?.name || '';
const nameB = nftB.display?.name || '';

if (nameA < nameB) {
return -1;
} else if (nameA > nameB) {
return 1;
}
return 0;
}) ?? []
);
}, [ownedAssets]);

// Fetch the next page if the user scrolls to the bottom of the page
useEffect(() => {
if (isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [isIntersecting, fetchNextPage, hasNextPage, isFetchingNextPage]);

// select the default category if no category is selected and assets are loaded
useEffect(() => {
let computeSelectedCategory = false;
if (
(selectedAssetCategory === AssetCategory.Visual && ownedAssets?.visual.length === 0) ||
(selectedAssetCategory === AssetCategory.Other && ownedAssets?.other.length === 0) ||
(selectedAssetCategory === AssetCategory.Hidden && ownedAssets?.hidden.length === 0) ||
!selectedAssetCategory
) {
computeSelectedCategory = true;
}
if (computeSelectedCategory && ownedAssets) {
const defaultCategory =
ownedAssets.visual.length > 0
? AssetCategory.Visual
: ownedAssets.other.length > 0
? AssetCategory.Other
: ownedAssets.hidden.length > 0
? AssetCategory.Hidden
: null;
setSelectedAssetCategory(defaultCategory);
}
}, [ownedAssets]);

// Fetch the next page if there are no visual assets, other + hidden assets are present in multiples of 50, and there are more pages to fetch
useEffect(() => {
if (
hasNextPage &&
ownedAssets?.visual.length === 0 &&
ownedAssets?.other.length + ownedAssets?.hidden.length > 0 &&
(ownedAssets.other.length + ownedAssets.hidden.length) % 50 === 0 &&
!isFetchingNextPage
) {
fetchNextPage();
setSelectedAssetCategory(null);
}
}, [hasNextPage, ownedAssets, isFetchingNextPage]);

return {
// reexport from useGetNFTs
ownedAssets,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
error,
isPending,
isError,
isFetching,
refetch,

isAssetsLoaded,
filteredAssets,
filteredHiddenAssets,
selectedAssetCategory,
setSelectedAssetCategory,
observerElem,
isSpinnerVisible,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

import { hasDisplayData, isKioskOwnerToken, useGetOwnedObjects, useKioskClient } from '@iota/core';
import {
hasDisplayData,
isKioskOwnerToken,
useGetOwnedObjects,
useKioskClient,
HiddenAssets,
COIN_TYPE,
} from '../../';
import { type IotaObjectData } from '@iota/iota-sdk/client';
import { useMemo } from 'react';
import { useHiddenAssets } from '../pages/home/assets/HiddenAssetsProvider';

type OwnedAssets = {
visual: IotaObjectData[];
Expand All @@ -18,25 +24,28 @@ export enum AssetFilterTypes {
Other = 'other',
}

export function useGetNFTs(address?: string | null) {
const OBJECTS_PER_REQ = 50;

export function useGetNFTs(address?: string | null, hiddenAssets?: HiddenAssets) {
const kioskClient = useKioskClient();
const {
data,
isFetching,
isPending,
error,
isError,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
isLoading,
refetch,
} = useGetOwnedObjects(
address,
{
MatchNone: [{ StructType: '0x2::coin::Coin' }],
MatchNone: [{ StructType: COIN_TYPE }],
},
50,
OBJECTS_PER_REQ,
);
const { hiddenAssets } = useHiddenAssets();

const assets = useMemo(() => {
const ownedAssets: OwnedAssets = {
Expand All @@ -45,13 +54,16 @@ export function useGetNFTs(address?: string | null) {
hidden: [],
};

if (hiddenAssets.type === 'loading') {
if (hiddenAssets?.type === 'loading') {
return ownedAssets;
} else {
const groupedAssets = data?.pages
.flatMap((page) => page.data)
.reduce((acc, curr) => {
if (curr.data?.objectId && hiddenAssets.assetIds.includes(curr.data?.objectId))
if (
curr.data?.objectId &&
hiddenAssets?.assetIds?.includes(curr.data?.objectId)
)
acc.hidden.push(curr.data as IotaObjectData);
else if (hasDisplayData(curr) || isKioskOwnerToken(kioskClient.network, curr))
acc.visual.push(curr.data as IotaObjectData);
Expand All @@ -64,12 +76,14 @@ export function useGetNFTs(address?: string | null) {

return {
data: assets,
isFetching,
isLoading,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
isPending: isPending,
isError: isError,
error,
refetch,
};
}
2 changes: 1 addition & 1 deletion apps/core/src/hooks/useOnScreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const useOnScreen = (elementRef: MutableRefObject<Element | null>) => {
);
observer.observe(node);
return () => observer.disconnect();
}, [elementRef]);
}, [elementRef.current]);

return { isIntersecting };
};
Loading

0 comments on commit 55b6b80

Please sign in to comment.