From 424af091096544dbb5da38ce15890a50cbdb8b8a Mon Sep 17 00:00:00 2001 From: Rohan-cp Date: Mon, 29 Apr 2024 23:29:24 +0530 Subject: [PATCH 01/13] followup: remove opacity dip for trending user card mobile (#2450) * base * base --- .../src/components/GalleryTouchableOpacity.tsx | 4 +++- .../components/ProfilePicture/ProfilePicture.tsx | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/components/GalleryTouchableOpacity.tsx b/apps/mobile/src/components/GalleryTouchableOpacity.tsx index b63e9016d7..c8f8a8f335 100644 --- a/apps/mobile/src/components/GalleryTouchableOpacity.tsx +++ b/apps/mobile/src/components/GalleryTouchableOpacity.tsx @@ -13,6 +13,7 @@ import { GalleryElementTrackingProps, useTrack } from '~/shared/contexts/Analyti export type GalleryTouchableOpacityProps = { withoutFeedback?: boolean; + activeOpacity?: number; } & GalleryElementTrackingProps & TouchableOpacityProps; @@ -25,6 +26,7 @@ export function GalleryTouchableOpacity({ onPress, properties, withoutFeedback, + activeOpacity = 0.2, ...props }: GalleryTouchableOpacityProps) { const track = useTrack(); @@ -62,7 +64,7 @@ export function GalleryTouchableOpacity({ } return ( - + {children} ); diff --git a/apps/mobile/src/components/ProfilePicture/ProfilePicture.tsx b/apps/mobile/src/components/ProfilePicture/ProfilePicture.tsx index 094e2b4489..94c8138e69 100644 --- a/apps/mobile/src/components/ProfilePicture/ProfilePicture.tsx +++ b/apps/mobile/src/components/ProfilePicture/ProfilePicture.tsx @@ -95,6 +95,19 @@ export function ProfilePicture({ userRef, style, ...rest }: ProfilePictureProps) /> ); + } else if (token) { + return ( + + + + ); } else { return fallbackProfilePicture; } From 3c49cf0e7fff117f8573a5f1f277f8ef9600db2c Mon Sep 17 00:00:00 2001 From: Jakz Date: Wed, 1 May 2024 14:09:11 +0800 Subject: [PATCH 02/13] Add multiple token into gallery editor (#2451) --- .../src/components/AnimatedRefreshIcon.tsx | 5 +- apps/mobile/src/components/Button.tsx | 8 +- apps/mobile/src/components/IconContainer.tsx | 5 +- .../NftSelectorContractHeader.tsx | 9 +- .../NftSelectorContractPickerGrid.tsx | 21 ++- .../NftSelectorContractToolbar.tsx | 78 +++++++++ .../NftSelector/NftSelectorHeader.tsx | 10 +- .../NftSelectorSelectionIndicator.tsx | 30 ++++ .../NftSelector/NftSelectorToolbar.tsx | 19 +++ apps/mobile/src/icons/CheckIcon.tsx | 9 + apps/mobile/src/icons/CheckboxIcon.tsx | 1 - apps/mobile/src/icons/MultiSelectIcon.tsx | 10 ++ .../src/navigation/RootStackNavigator.tsx | 5 + apps/mobile/src/navigation/types.ts | 5 + .../GalleryEditorNftSelector.tsx | 105 ++++++++++-- ...GalleryEditorNftSelectorContractScreen.tsx | 160 ++++++++++++++++++ .../NftSelectorPickerGrid.tsx | 69 +++++++- .../NftSelectorPickerSingularAsset.tsx | 7 + 18 files changed, 523 insertions(+), 33 deletions(-) create mode 100644 apps/mobile/src/components/NftSelector/NftSelectorContract/NftSelectorContractToolbar.tsx create mode 100644 apps/mobile/src/components/NftSelector/NftSelectorSelectionIndicator.tsx create mode 100644 apps/mobile/src/icons/CheckIcon.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/AnimatedRefreshIcon.tsx b/apps/mobile/src/components/AnimatedRefreshIcon.tsx index 844c6f4be1..67cf8b705f 100644 --- a/apps/mobile/src/components/AnimatedRefreshIcon.tsx +++ b/apps/mobile/src/components/AnimatedRefreshIcon.tsx @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { ViewProps } from 'react-native'; import { RefreshIcon } from 'src/icons/RefreshIcon'; import { IconContainer } from '~/components/IconContainer'; @@ -9,6 +10,7 @@ type AnimatedRefreshIconProps = { isSyncing: boolean; eventElementId: string; eventName: string; + style?: ViewProps['style']; }; export function AnimatedRefreshIcon({ @@ -16,6 +18,7 @@ export function AnimatedRefreshIcon({ isSyncing, eventElementId, eventName, + style, }: AnimatedRefreshIconProps) { const handleSync = useCallback(async () => { if (isSyncing) return; @@ -30,7 +33,7 @@ export function AnimatedRefreshIcon({ eventElementId={eventElementId} eventName={eventName} eventContext={contexts.Posts} - style={{ opacity: isSyncing ? 0.3 : 1 }} + style={[{ opacity: isSyncing ? 0.3 : 1 }, style]} /> ); } diff --git a/apps/mobile/src/components/Button.tsx b/apps/mobile/src/components/Button.tsx index 403137fdc2..cdfb157ff1 100644 --- a/apps/mobile/src/components/Button.tsx +++ b/apps/mobile/src/components/Button.tsx @@ -11,6 +11,7 @@ import { Typography } from './Typography'; type Variant = 'primary' | 'secondary' | 'danger' | 'disabled' | 'blue'; type FontWeight = 'Medium' | 'Regular' | 'Bold'; type Size = 'xs' | 'sm' | 'md'; +type TextTransform = 'uppercase' | 'capitalize' | 'lowercase'; export type ButtonProps = { style?: GalleryTouchableOpacityProps['style']; @@ -24,6 +25,7 @@ export type ButtonProps = { textClassName?: string; containerClassName?: string; fontWeight?: FontWeight; + textTransform?: TextTransform; size?: Size; DO_NOT_USE_OR_YOU_WILL_BE_FIRED_colorScheme?: 'light' | 'dark'; } & GalleryTouchableOpacityProps; @@ -179,6 +181,7 @@ export function Button({ size = 'md', fontWeight = 'Medium', footerElement, + textTransform = 'uppercase', DO_NOT_USE_OR_YOU_WILL_BE_FIRED_colorScheme, ...props }: ButtonProps) { @@ -226,9 +229,10 @@ export function Button({ {text} diff --git a/apps/mobile/src/components/IconContainer.tsx b/apps/mobile/src/components/IconContainer.tsx index 092ef33ea7..e7c4caa650 100644 --- a/apps/mobile/src/components/IconContainer.tsx +++ b/apps/mobile/src/components/IconContainer.tsx @@ -10,7 +10,7 @@ type IconContainerProps = { onPress: () => void; size?: 'xs' | 'sm' | 'md'; border?: boolean; - color?: 'default' | 'white' | 'black'; + color?: 'default' | 'white' | 'black' | 'active'; } & GalleryTouchableOpacityProps; export function IconContainer({ @@ -28,10 +28,11 @@ export function IconContainer({ md: 'h-8 w-8', }; - const colorVariants: { [color in 'default' | 'white' | 'black']: string } = { + const colorVariants: { [color in 'default' | 'white' | 'black' | 'active']: string } = { default: 'bg-faint dark:bg-black-500', white: 'bg-white dark:bg-black-900', black: 'bg-black-900 dark:bg-white', + active: 'bg-porcelain dark:bg-white', }; return ( diff --git a/apps/mobile/src/components/NftSelector/NftSelectorContract/NftSelectorContractHeader.tsx b/apps/mobile/src/components/NftSelector/NftSelectorContract/NftSelectorContractHeader.tsx index 65aadd2b1e..cc605a24a5 100644 --- a/apps/mobile/src/components/NftSelector/NftSelectorContract/NftSelectorContractHeader.tsx +++ b/apps/mobile/src/components/NftSelector/NftSelectorContract/NftSelectorContractHeader.tsx @@ -1,5 +1,6 @@ import { useCallback } from 'react'; import { View, ViewProps } from 'react-native'; +import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'; import { AnimatedRefreshIcon } from '~/components/AnimatedRefreshIcon'; import { BackButton } from '~/components/BackButton'; @@ -24,6 +25,12 @@ export function NftSelectorContractHeader({ const { isSyncingCreatedTokensForContract, syncCreatedTokensForExistingContract } = useSyncTokensActions(); + const animateStyle = useAnimatedStyle(() => { + return { + opacity: withTiming(rightButton ? 1 : 0), + }; + }); + const handleSyncTokensForContract = useCallback(async () => { if (!contractId) return; syncCreatedTokensForExistingContract(contractId); @@ -47,7 +54,7 @@ export function NftSelectorContractHeader({ {title} - {rightButton ? {rightButton} : null} + {rightButton} {isCreator && contractId ? ( void; style?: ViewProps['style']; + + isMultiselectMode?: boolean; + selectedTokens?: Set; }; -export function NftSelectorContractPickerGrid({ isCreator, tokenRefs, onSelect, style }: Props) { +export function NftSelectorContractPickerGrid({ + isCreator, + isMultiselectMode, + tokenRefs, + onSelect, + selectedTokens, + style, +}: Props) { const tokens = useFragment( graphql` fragment NftSelectorContractPickerGridFragment on Token @relay(plural: true) { @@ -43,6 +53,10 @@ export function NftSelectorContractPickerGrid({ isCreator, tokenRefs, onSelect, syncCreatedTokensForExistingContract(contractId); }, [syncCreatedTokensForExistingContract, contractId]); + const isSelected = useMemo(() => { + return (tokenId: string) => selectedTokens?.has(tokenId) ?? false; + }, [selectedTokens]); + const renderItem = useCallback>( ({ item: row }) => { return ( @@ -53,6 +67,8 @@ export function NftSelectorContractPickerGrid({ isCreator, tokenRefs, onSelect, key={token.dbid} onPress={onSelect} tokenRef={token} + isMultiselectMode={isMultiselectMode} + isSelected={isSelected(token.dbid)} /> ); })} @@ -63,7 +79,7 @@ export function NftSelectorContractPickerGrid({ isCreator, tokenRefs, onSelect, ); }, - [onSelect] + [isMultiselectMode, isSelected, onSelect] ); const rows = useMemo(() => { @@ -83,6 +99,7 @@ export function NftSelectorContractPickerGrid({ isCreator, tokenRefs, onSelect, renderItem={renderItem} data={rows} estimatedItemSize={100} + extraData={[isMultiselectMode, selectedTokens]} refreshControl={ isCreator ? ( void; + onSelectedAllPress: () => void; + hasSelectedItems: boolean; + ownershipTypeFilter: 'Collected' | 'Created'; +}; + +export function NftSelectorContractToolbar({ + contractId, + contractName, + isMultiselectMode, + setIsMultiselectMode, + onSelectedAllPress, + hasSelectedItems, + ownershipTypeFilter, +}: Props) { + const { isSyncing, isSyncingCreatedTokens, syncCreatedTokensForExistingContract } = + useSyncTokensActions(); + + const handleMultiselectPress = useCallback(() => { + setIsMultiselectMode?.(!isMultiselectMode); + }, [isMultiselectMode, setIsMultiselectMode]); + + const handleSyncTokensForContract = useCallback(async () => { + if (!contractId) return; + syncCreatedTokensForExistingContract(contractId); + }, [syncCreatedTokensForExistingContract, contractId]); + + return ( + + + {contractName} + + + + + + {hasSelectedItems ? 'Deselect All' : 'Select All'} + + + } + eventElementId="NftSelectorSelectorSettingsButton" + eventName="NftSelectorSelectorSettingsButton pressed" + eventContext={contexts.Posts} + color={isMultiselectMode ? 'active' : 'default'} + /> + + + + ); +} diff --git a/apps/mobile/src/components/NftSelector/NftSelectorHeader.tsx b/apps/mobile/src/components/NftSelector/NftSelectorHeader.tsx index ae5341233d..d364a7c9af 100644 --- a/apps/mobile/src/components/NftSelector/NftSelectorHeader.tsx +++ b/apps/mobile/src/components/NftSelector/NftSelectorHeader.tsx @@ -1,5 +1,6 @@ import { PropsWithChildren } from 'react'; import { View, ViewProps } from 'react-native'; +import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'; import { BackButton } from '../BackButton'; import { Typography } from '../Typography'; @@ -11,6 +12,12 @@ type Props = { } & PropsWithChildren; export function NftSelectorHeader({ rightButton, title, children, style }: Props) { + const animateStyle = useAnimatedStyle(() => { + return { + opacity: withTiming(rightButton ? 1 : 0), + }; + }); + return ( @@ -24,8 +31,7 @@ export function NftSelectorHeader({ rightButton, title, children, style }: Props {title} - - {rightButton && rightButton} + {rightButton && rightButton} {children} 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/icons/CheckboxIcon.tsx b/apps/mobile/src/icons/CheckboxIcon.tsx index b94dbde0e6..dee4aadbaa 100644 --- a/apps/mobile/src/icons/CheckboxIcon.tsx +++ b/apps/mobile/src/icons/CheckboxIcon.tsx @@ -10,7 +10,6 @@ export function CheckboxIcon(props: SvgProps) { return ( - ); } diff --git a/apps/mobile/src/icons/MultiSelectIcon.tsx b/apps/mobile/src/icons/MultiSelectIcon.tsx new file mode 100644 index 0000000000..44888929d3 --- /dev/null +++ b/apps/mobile/src/icons/MultiSelectIcon.tsx @@ -0,0 +1,10 @@ +import Svg, { Path, SvgProps } from 'react-native-svg'; + +export function MultiSelectIcon(props: SvgProps) { + return ( + + + + + ); +} 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..6544fc2cf3 100644 --- a/apps/mobile/src/navigation/types.ts +++ b/apps/mobile/src/navigation/types.ts @@ -30,6 +30,11 @@ export type RootStackNavigatorParamList = { NftSelectorGalleryEditor: { galleryId: string; }; + NftSelectorContractGalleryEditor: { + galleryId: string; + 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..5cebc94fa9 100644 --- a/apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelector.tsx +++ b/apps/mobile/src/screens/GalleryScreen/GalleryEditorNftSelector.tsx @@ -1,12 +1,13 @@ import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; -import { Suspense, useCallback, useMemo } from 'react'; +import { Suspense, useCallback, useMemo, useState } from 'react'; import { View } from 'react-native'; -import { noop } from 'shared/utils/noop'; +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'; @@ -29,20 +30,78 @@ export function GalleryEditorNftSelector() { const route = useRoute>(); + const [isMultiselectMode, setIsMultiselectMode] = useState(false); + + const [selectedTokens, setSelectedTokens] = useState>(new Set()); + const handleSelectNft = useCallback( (tokenId: string) => { - navigation.navigate({ - name: 'GalleryEditor', - params: { - galleryId: route.params.galleryId, - stagedTokens: [tokenId], - }, - merge: true, - }); + if (isMultiselectMode) { + setSelectedTokens((prevTokens) => { + const newTokens = new Set(prevTokens); + + if (newTokens.has(tokenId)) { + newTokens.delete(tokenId); + } else { + newTokens.add(tokenId); + } + return newTokens; + }); + } 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) { + setSelectedTokens((prevTokens) => { + const newTokens = new Set(prevTokens); + + tokens.forEach((token) => { + if (newTokens.has(token.dbid)) { + newTokens.delete(token.dbid); + } else { + newTokens.add(token.dbid); + } + }); + return newTokens; + }); + } else { + navigation.navigate({ + name: 'NftSelectorContractGalleryEditor', + params: { + galleryId: route.params.galleryId, + contractAddress, + }, + }); + } }, - [navigation, route.params.galleryId] + [isMultiselectMode, navigation, route.params.galleryId] ); + const handleAddSelectedTokens = useCallback(() => { + const formattedTokens = Array.from(selectedTokens); + + navigation.navigate({ + name: 'GalleryEditor', + params: { + galleryId: route.params.galleryId, + stagedTokens: formattedTokens, + }, + merge: true, + }); + }, [navigation, route.params.galleryId, selectedTokens]); + const searchCriteria = useMemo( () => ({ searchQuery, @@ -56,7 +115,23 @@ export function GalleryEditorNftSelector() { return ( - + 0 ? ( +