From 1f69a5b602f711daa80f3c30fbade1e55c7f89f6 Mon Sep 17 00:00:00 2001 From: Jakz Date: Mon, 29 Apr 2024 18:47:44 +0800 Subject: [PATCH] Able to add multiple token into gallery --- .../NftSelectorSelectionIndicator.tsx | 30 ++++ .../NftSelector/NftSelectorToolbar.tsx | 19 +++ apps/mobile/src/icons/MultiSelectIcon.tsx | 10 ++ .../src/navigation/RootStackNavigator.tsx | 5 + apps/mobile/src/navigation/types.ts | 4 + .../GalleryEditorNftSelector.tsx | 128 ++++++++++++++++-- ...GalleryEditorNftSelectorContractScreen.tsx | 41 ++++++ .../NftSelectorPickerGrid.tsx | 78 +++++++++-- .../NftSelectorPickerSingularAsset.tsx | 7 + 9 files changed, 299 insertions(+), 23 deletions(-) create mode 100644 apps/mobile/src/components/NftSelector/NftSelectorSelectionIndicator.tsx create mode 100644 apps/mobile/src/icons/MultiSelectIcon.tsx create mode 100644 apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelectorContractScreen.tsx diff --git a/apps/mobile/src/components/NftSelector/NftSelectorSelectionIndicator.tsx b/apps/mobile/src/components/NftSelector/NftSelectorSelectionIndicator.tsx new file mode 100644 index 0000000000..63b96bd248 --- /dev/null +++ b/apps/mobile/src/components/NftSelector/NftSelectorSelectionIndicator.tsx @@ -0,0 +1,30 @@ +import clsx from 'clsx'; +import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'; +import { CheckIcon } from 'src/icons/CheckIcon'; + +type Props = { + selected: boolean; +}; + +export function NftSelectorSelectionIndicator({ selected }: Props) { + const animateStyle = useAnimatedStyle(() => { + return { + opacity: withTiming(selected ? 1 : 0), + }; + }); + + return ( + + {selected ? ( + + + + ) : null} + + ); +} diff --git a/apps/mobile/src/components/NftSelector/NftSelectorToolbar.tsx b/apps/mobile/src/components/NftSelector/NftSelectorToolbar.tsx index d5f55218b9..53355f9029 100644 --- a/apps/mobile/src/components/NftSelector/NftSelectorToolbar.tsx +++ b/apps/mobile/src/components/NftSelector/NftSelectorToolbar.tsx @@ -2,6 +2,7 @@ import { useCallback, useMemo } from 'react'; import { View, ViewProps } from 'react-native'; import { contexts } from 'shared/analytics/constants'; import { chains } from 'shared/utils/chains'; +import { MultiSelectIcon } from 'src/icons/MultiSelectIcon'; import { SlidersIcon } from 'src/icons/SlidersIcon'; import { getChainIconComponent } from 'src/utils/getChainIconComponent'; @@ -41,6 +42,8 @@ type Props = { setNetworkFilter: (value: NetworkChoice) => void; sortView: NftSelectorSortView; setSortView: (value: NftSelectorSortView) => void; + isMultiselectMode?: boolean; + setIsMultiselectMode?: (value: boolean) => void; isSyncing: boolean; isSyncingCreatedTokens: boolean; handleSync: () => void; @@ -56,6 +59,8 @@ export function NftSelectorToolbar({ setNetworkFilter, sortView, setSortView, + isMultiselectMode, + setIsMultiselectMode, isSyncing, isSyncingCreatedTokens, handleSync, @@ -92,6 +97,10 @@ export function NftSelectorToolbar({ [setNetworkFilter] ); + const handleMultiselectPress = useCallback(() => { + setIsMultiselectMode?.(!isMultiselectMode); + }, [isMultiselectMode, setIsMultiselectMode]); + return ( @@ -116,6 +125,16 @@ export function NftSelectorToolbar({ /> + } + eventElementId="NftSelectorSelectorSettingsButton" + eventName="NftSelectorSelectorSettingsButton pressed" + eventContext={contexts.Posts} + color={isMultiselectMode ? 'active' : 'default'} + /> + + + + + ); +} diff --git a/apps/mobile/src/navigation/RootStackNavigator.tsx b/apps/mobile/src/navigation/RootStackNavigator.tsx index d9a098ec69..ce81fdac0f 100644 --- a/apps/mobile/src/navigation/RootStackNavigator.tsx +++ b/apps/mobile/src/navigation/RootStackNavigator.tsx @@ -17,6 +17,7 @@ import { RootStackNavigatorParamList } from '~/navigation/types'; import { Debugger } from '~/screens/Debugger'; import { DesignSystemButtonsScreen } from '~/screens/DesignSystemButtonsScreen'; import { GalleryEditorNftSelector } from '~/screens/GalleryScreen/GalleryEditorNftSelector'; +import { GalleryEditorNftSelectorContractScreen } from '~/screens/GalleryScreen/GalleryEditorNftSelectorContractScreen'; import { GalleryEditorScreen } from '~/screens/GalleryScreen/GalleryEditorScreen'; import { TwitterSuggestionListScreen } from '~/screens/HomeScreen/TwitterSuggestionListScreen'; import { UserSuggestionListScreen } from '~/screens/HomeScreen/UserSuggestionListScreen'; @@ -105,6 +106,10 @@ export function RootStackNavigator({ navigationContainerRef }: Props) { + diff --git a/apps/mobile/src/navigation/types.ts b/apps/mobile/src/navigation/types.ts index 2aac104578..fe136c6f5d 100644 --- a/apps/mobile/src/navigation/types.ts +++ b/apps/mobile/src/navigation/types.ts @@ -30,6 +30,10 @@ export type RootStackNavigatorParamList = { NftSelectorGalleryEditor: { galleryId: string; }; + NftSelectorContractGalleryEditor: { + contractAddress: string; + ownerFilter?: 'Collected' | 'Created'; + }; }; export type ScreenWithNftSelector = 'ProfilePicture' | 'Post' | 'Community' | 'Onboarding'; diff --git a/apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelector.tsx b/apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelector.tsx index 2956365b23..1367ff04f1 100644 --- a/apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelector.tsx +++ b/apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelector.tsx @@ -1,16 +1,22 @@ import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; -import { Suspense, useCallback, useMemo } from 'react'; -import { View } from 'react-native'; -import { noop } from 'shared/utils/noop'; +import { Suspense, useCallback, useMemo, useState } from 'react'; +import { Text, View } from 'react-native'; +import { Button } from '~/components/Button'; import { NftSelectorHeader } from '~/components/NftSelector/NftSelectorHeader'; import { NftSelectorToolbar } from '~/components/NftSelector/NftSelectorToolbar'; import { NftSelectorWrapper } from '~/components/NftSelector/NftSelectorWrapper'; import { useNftSelector } from '~/components/NftSelector/useNftSelector'; +import { NftSelectorPickerGridTokenGridFragment$data } from '~/generated/NftSelectorPickerGridTokenGridFragment.graphql'; import { RootStackNavigatorParamList, RootStackNavigatorProp } from '~/navigation/types'; import { NftSelectorLoadingSkeleton } from '~/screens/NftSelectorScreen/NftSelectorLoadingSkeleton'; import { NftSelectorPickerGrid } from '~/screens/NftSelectorScreen/NftSelectorPickerGrid'; +export type SelectedItemMultiMode = { + id: string; + contractAddress?: string; +}; + export function GalleryEditorNftSelector() { const { searchQuery, @@ -29,20 +35,97 @@ export function GalleryEditorNftSelector() { const route = useRoute>(); + const [isMultiselectMode, setIsMultiselectMode] = useState(false); + + const [selectedTokens, setSelectedTokens] = useState([]); + const handleSelectNft = useCallback( (tokenId: string) => { - navigation.navigate({ - name: 'GalleryEditor', - params: { - galleryId: route.params.galleryId, - stagedTokens: [tokenId], - }, - merge: true, - }); + if (isMultiselectMode) { + setSelectedTokens((prevTokens) => { + if ( + prevTokens.some((token) => { + return token.id === tokenId; + }) + ) { + return [ + ...prevTokens.filter((token) => { + return token.id !== tokenId; + }), + ]; + } else { + return [ + ...prevTokens, + { + id: tokenId, + }, + ]; + } + }); + } else { + navigation.navigate({ + name: 'GalleryEditor', + params: { + galleryId: route.params.galleryId, + stagedTokens: [tokenId], + }, + merge: true, + }); + } + }, + [isMultiselectMode, navigation, route.params.galleryId] + ); + + const handleSelectNftGroup = useCallback( + (contractAddress: string, tokens: NftSelectorPickerGridTokenGridFragment$data[number][]) => { + if (isMultiselectMode) { + const formattedTokens = tokens.map((token) => { + return { + id: token.dbid, + contractAddress, + }; + }); + + setSelectedTokens((prevTokens) => { + if ( + prevTokens.some((token) => { + return token.contractAddress === contractAddress; + }) + ) { + return [ + ...prevTokens.filter((token) => { + return token.contractAddress !== contractAddress; + }), + ]; + } else { + return [...prevTokens, ...formattedTokens]; + } + }); + } else { + navigation.navigate({ + name: 'NftSelectorContractGalleryEditor', + params: { + contractAddress, + }, + }); + } }, - [navigation, route.params.galleryId] + [isMultiselectMode, navigation] ); + const handleAddSelectedTokens = useCallback(() => { + const formattedTokens = selectedTokens.map((token) => token.id); + + navigation.navigate({ + name: 'GalleryEditor', + params: { + galleryId: route.params.galleryId, + stagedTokens: formattedTokens, + }, + merge: true, + }); + }, [navigation, route.params.galleryId, selectedTokens]); + const searchCriteria = useMemo( () => ({ searchQuery, @@ -56,7 +139,20 @@ export function GalleryEditorNftSelector() { return ( - + + } + /> + {JSON.stringify(selectedTokens)} @@ -77,7 +175,9 @@ export function GalleryEditorNftSelector() { searchCriteria={searchCriteria} onRefresh={sync} onSelect={handleSelectNft} - onSelectNftGroup={noop} + onSelectNftGroup={handleSelectNftGroup} + isMultiselectMode={isMultiselectMode} + selectedTokens={selectedTokens} /> diff --git a/apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelectorContractScreen.tsx b/apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelectorContractScreen.tsx new file mode 100644 index 0000000000..413eb61b56 --- /dev/null +++ b/apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelectorContractScreen.tsx @@ -0,0 +1,41 @@ +import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; +import { useCallback } from 'react'; +import { graphql, useLazyLoadQuery } from 'react-relay'; + +import { NftSelectorContract } from '~/components/NftSelector/NftSelectorContract/NftSelectorContract'; +import { GalleryEditorNftSelectorContractScreenQuery } from '~/generated/GalleryEditorNftSelectorContractScreenQuery.graphql'; +import { RootStackNavigatorParamList, RootStackNavigatorProp } from '~/navigation/types'; + +export function GalleryEditorNftSelectorContractScreen() { + const query = useLazyLoadQuery( + graphql` + query GalleryEditorNftSelectorContractScreenQuery { + ...NftSelectorContractFragment + } + `, + {} + ); + + const navigation = useNavigation(); + const route = + useRoute>(); + + const handleSelectNft = useCallback( + (tokenId: string) => { + navigation.navigate('PostComposer', { + tokenId, + }); + }, + [navigation] + ); + + return ( + + ); +} diff --git a/apps/mobile/src/screens/NftSelectorScreen/NftSelectorPickerGrid.tsx b/apps/mobile/src/screens/NftSelectorScreen/NftSelectorPickerGrid.tsx index 9425f8b92d..3ce4a3c3ec 100644 --- a/apps/mobile/src/screens/NftSelectorScreen/NftSelectorPickerGrid.tsx +++ b/apps/mobile/src/screens/NftSelectorScreen/NftSelectorPickerGrid.tsx @@ -13,6 +13,7 @@ import { GalleryRefreshControl } from '~/components/GalleryRefreshControl'; import { GallerySkeleton } from '~/components/GallerySkeleton'; import { GalleryTouchableOpacity } from '~/components/GalleryTouchableOpacity'; import { NftPreviewAssetToWrapInBoundary } from '~/components/NftPreview/NftPreviewAsset'; +import { NftSelectorSelectionIndicator } from '~/components/NftSelector/NftSelectorSelectionIndicator'; import { Typography } from '~/components/Typography'; import { useManageWalletActions } from '~/contexts/ManageWalletContext'; import { useSyncTokensActions } from '~/contexts/SyncTokensContext'; @@ -38,6 +39,7 @@ import { contexts } from '~/shared/analytics/constants'; import { removeNullValues } from '~/shared/relay/removeNullValues'; import { doesUserOwnWalletFromChainFamily } from '~/shared/utils/doesUserOwnWalletFromChainFamily'; +import { SelectedItemMultiMode } from '../GalleryScreen/GalleryEditorNftSelector'; import { NftSelectorLoadingSkeleton } from './NftSelectorLoadingSkeleton'; type NftSelectorPickerGridProps = { @@ -48,15 +50,24 @@ type NftSelectorPickerGridProps = { networkFilter: NetworkChoice; sortView: NftSelectorSortView; }; + + isMultiselectMode?: boolean; + selectedTokens?: SelectedItemMultiMode[]; + onRefresh: () => void; onSelect: (tokenId: string) => void; - onSelectNftGroup: (contractAddress: string) => void; + onSelectNftGroup: ( + contractAddress: string, + tokens: NftSelectorPickerGridTokenGridFragment$data[number][] + ) => void; }; export function NftSelectorPickerGrid({ searchCriteria, style, + isMultiselectMode, + selectedTokens = [], onRefresh, onSelect, onSelectNftGroup, @@ -315,6 +326,8 @@ export function NftSelectorPickerGrid({ ownerFilter={searchCriteria.ownerFilter} onSelectNft={onSelect} onSelectGroup={onSelectNftGroup} + isMultiselectMode={isMultiselectMode} + selectedTokens={selectedTokens} /> ); })} @@ -326,7 +339,7 @@ export function NftSelectorPickerGrid({ ); }, - [onSelect, onSelectNftGroup, searchCriteria.ownerFilter] + [isMultiselectMode, onSelect, onSelectNftGroup, searchCriteria.ownerFilter, selectedTokens] ); const navigation = useNavigation(); @@ -387,6 +400,7 @@ export function NftSelectorPickerGrid({ refreshControl={ } + extraData={[isMultiselectMode, selectedTokens]} /> ); @@ -436,10 +450,22 @@ type TokenGridProps = { tokenRefs: NftSelectorPickerGridTokenGridFragment$key; ownerFilter: 'Created' | 'Collected'; contractAddress: string; - onPress: (contractAddress: string) => void; + isMultiselectMode?: boolean; + isSelected?: boolean; + onPress: ( + contractAddress: string, + tokens: NftSelectorPickerGridTokenGridFragment$data[number][] + ) => void; }; -function TokenGrid({ tokenRefs, contractAddress, style, onPress }: TokenGridProps) { +function TokenGrid({ + tokenRefs, + contractAddress, + style, + isMultiselectMode, + onPress, + isSelected = false, +}: TokenGridProps) { const tokens = useFragment( graphql` fragment NftSelectorPickerGridTokenGridFragment on Token @relay(plural: true) { @@ -460,8 +486,8 @@ function TokenGrid({ tokenRefs, contractAddress, style, onPress }: TokenGridProp }, [tokens]); const handlePress = useCallback(() => { - onPress(contractAddress); - }, [contractAddress, onPress]); + onPress(contractAddress, removeNullValues(tokens)); + }, [contractAddress, onPress, tokens]); return ( + + {isMultiselectMode && } ); } @@ -500,7 +528,12 @@ type TokenGroupProps = { tokenRefs: NftSelectorPickerGridOneOrManyFragment$key; contractAddress: string; onSelectNft: (tokenId: string) => void; - onSelectGroup: (contractAddress: string) => void; + onSelectGroup: ( + contractAddress: string, + tokens: NftSelectorPickerGridTokenGridFragment$data[number][] + ) => void; + isMultiselectMode?: boolean; + selectedTokens?: SelectedItemMultiMode[]; }; function TokenGroup({ @@ -510,10 +543,13 @@ function TokenGroup({ ownerFilter, onSelectNft, onSelectGroup, + isMultiselectMode, + selectedTokens = [], }: TokenGroupProps) { const tokens = useFragment( graphql` fragment NftSelectorPickerGridOneOrManyFragment on Token @relay(plural: true) { + dbid ...NftSelectorPickerGridTokenGridFragment ...NftSelectorPickerSingularAssetFragment } @@ -522,20 +558,44 @@ function TokenGroup({ ); const [firstToken] = tokens; + + const isSingleView = useMemo(() => { + return tokens.length === 1; + }, [tokens]); + + const isSelected = useMemo(() => { + if (isSingleView) { + return selectedTokens.some((token) => { + return token.id === firstToken?.dbid; + }); + } else { + return selectedTokens.some((token) => { + return token.id === contractAddress; + }); + } + }, [selectedTokens, contractAddress, firstToken, isSingleView]); + if (!firstToken) { return null; } return ( - {tokens.length === 1 ? ( - + {isSingleView ? ( + ) : ( )} diff --git a/apps/mobile/src/screens/NftSelectorScreen/NftSelectorPickerSingularAsset.tsx b/apps/mobile/src/screens/NftSelectorScreen/NftSelectorPickerSingularAsset.tsx index 82a0ab17a7..77079744aa 100644 --- a/apps/mobile/src/screens/NftSelectorScreen/NftSelectorPickerSingularAsset.tsx +++ b/apps/mobile/src/screens/NftSelectorScreen/NftSelectorPickerSingularAsset.tsx @@ -9,6 +9,7 @@ import { TokenFailureBoundary } from '~/components/Boundaries/TokenFailureBounda import { GallerySkeleton } from '~/components/GallerySkeleton'; import { GalleryTouchableOpacity } from '~/components/GalleryTouchableOpacity'; import { NftPreviewAssetToWrapInBoundary } from '~/components/NftPreview/NftPreviewAsset'; +import { NftSelectorSelectionIndicator } from '~/components/NftSelector/NftSelectorSelectionIndicator'; import { NftSelectorPickerSingularAssetFragment$key } from '~/generated/NftSelectorPickerSingularAssetFragment.graphql'; import { contexts } from '~/shared/analytics/constants'; import colors from '~/shared/theme/colors'; @@ -18,6 +19,8 @@ type NftSelectorPickerSingularAssetProps = { tokenRef: NftSelectorPickerSingularAssetFragment$key; onPress: (tokenId: string) => void; isLoading?: boolean; + isMultiselectMode?: boolean; + isSelected?: boolean; }; export function NftSelectorPickerSingularAsset({ @@ -25,6 +28,8 @@ export function NftSelectorPickerSingularAsset({ tokenRef, onPress, isLoading, + isMultiselectMode, + isSelected = false, }: NftSelectorPickerSingularAssetProps) { const token = useFragment( graphql` @@ -83,6 +88,8 @@ export function NftSelectorPickerSingularAsset({ )} + + {isMultiselectMode && } );