Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[do not merge] Add all networks dropdown (web) #2354

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/mobile/src/components/AnimatedRefreshIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function AnimatedRefreshIcon({
eventElementId={eventElementId}
eventName={eventName}
eventContext={contexts.Posts}
disabled={isSyncing}
style={[{ opacity: isSyncing ? 0.3 : 1 }, style]}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function CommunityPostBottomSheet({ communityRef, onRefresh }: Pr
const { chain } = extractRelevantMetadataFromCommunity(community);

const handleSync = useCallback(async () => {
if (!chain) return;
if (!chain || chain === 'All Networks') return;

await syncTokens(chain);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { NeynarPayloadVariables } from 'shared/hooks/useAuthPayloadQuery';
import useCreateNonce from 'shared/hooks/useCreateNonce';
import { useGetUsersByWalletAddressesImperatively } from 'shared/hooks/useGetUserByWalletAddress';
import { removeNullValues } from 'shared/relay/removeNullValues';
import { Chain } from 'shared/utils/chains';
import { useLogin } from 'src/hooks/useLogin';

import { useBottomSheetModalActions } from '~/contexts/BottomSheetModalContext';
Expand Down Expand Up @@ -127,7 +126,7 @@ export function useLoginWithFarcaster() {
signature: req.signature,
custodyPubKey: {
pubKey: req.custody,
chain: 'Ethereum' as Chain,
chain: 'Ethereum',
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const NETWORKS: {
...chains.map((chain) => ({
label: chain.name,
id: chain.name,
icon: getChainIconComponent(chain),
icon: getChainIconComponent(chain.name),
hasCreatorSupport: chain.hasCreatorSupport,
})),
];
Expand Down
17 changes: 12 additions & 5 deletions apps/mobile/src/components/NftSelector/useNftSelector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { AvailableChains, chains } from 'shared/utils/chains';

import { useSyncTokensActions } from '~/contexts/SyncTokensContext';
import {
Expand All @@ -9,21 +10,27 @@ import {
export function useNftSelector() {
const [searchQuery, setSearchQuery] = useState<string>('');
const [ownershipTypeFilter, setFilter] = useState<'Collected' | 'Created'>('Collected');
const [networkFilter, setNetworkFilter] = useState<NetworkChoice>('Ethereum');
const [networkFilter, setNetworkFilter] = useState<NetworkChoice>('All Networks');
const [sortView, setSortView] = useState<NftSelectorSortView>('Recently added');
const [isDraggableMode, setIsDraggableMode] = useState(false);

const { syncTokens, syncCreatedTokens, isSyncing, isSyncingCreatedTokens } =
useSyncTokensActions();

const availableChains = useMemo(() => {
return chains
.filter((chain) => chain.name !== 'All Networks')
.map((chain) => chain.name as AvailableChains);
}, []);

const handleSync = useCallback(async () => {
if (ownershipTypeFilter === 'Collected') {
await syncTokens(networkFilter);
await syncTokens(networkFilter === 'All Networks' ? availableChains : networkFilter);
}
if (ownershipTypeFilter === 'Created') {
if (ownershipTypeFilter === 'Created' && networkFilter !== 'All Networks') {
await syncCreatedTokens(networkFilter);
}
}, [ownershipTypeFilter, syncTokens, networkFilter, syncCreatedTokens]);
}, [ownershipTypeFilter, networkFilter, syncTokens, availableChains, syncCreatedTokens]);

return {
searchQuery,
Expand Down
60 changes: 32 additions & 28 deletions apps/mobile/src/screens/NftSelectorScreen/NftSelectorPickerGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { View, ViewProps } from 'react-native';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
import { useFragment, useLazyLoadQuery, useRelayEnvironment } from 'react-relay';
import { fetchQuery, graphql } from 'relay-runtime';
import { Chain } from 'shared/utils/chains';

import { TokenFailureBoundary } from '~/components/Boundaries/TokenFailureBoundary/TokenFailureBoundary';
import { Button } from '~/components/Button';
Expand All @@ -30,10 +31,7 @@ import {
} from '~/generated/NftSelectorPickerGridTokensFragment.graphql';
import { NftSelectorPickerGridTokensQuery } from '~/generated/NftSelectorPickerGridTokensQuery.graphql';
import { LoginStackNavigatorProp } from '~/navigation/types';
import {
NetworkChoice,
NftSelectorSortView,
} from '~/screens/NftSelectorScreen/NftSelectorFilterBottomSheet';
import { NftSelectorSortView } from '~/screens/NftSelectorScreen/NftSelectorFilterBottomSheet';
import { NftSelectorPickerSingularAsset } from '~/screens/NftSelectorScreen/NftSelectorPickerSingularAsset';
import { contexts } from '~/shared/analytics/constants';
import { removeNullValues } from '~/shared/relay/removeNullValues';
Expand All @@ -46,7 +44,7 @@ type NftSelectorPickerGridProps = {
searchCriteria: {
searchQuery: string;
ownerFilter: 'Collected' | 'Created';
networkFilter: NetworkChoice;
networkFilter: Chain;
sortView: NftSelectorSortView;
};

Expand Down Expand Up @@ -179,31 +177,37 @@ export function NftSelectorPickerGrid({
// [GAL-4202] this logic could be consolidated across web editor + web selector + mobile selector
// but also don't overdo it if there's sufficient differentiation between web and mobile UX
const filteredTokens = useMemo(() => {
return tokensData
.filter((token) => {
const isSpam = token.definition?.contract?.isSpam || token.isSpamByUser;

return !isSpam;
})
.filter((token) => {
if (!searchCriteria.searchQuery) {
return true;
}
let filtered = tokensData;

return token.definition?.community?.name
?.toLowerCase()
.includes(searchCriteria.searchQuery.toLowerCase());
})
.filter((token) => {
return token.definition?.chain === searchCriteria.networkFilter;
})
.filter((token) => {
if (searchCriteria.ownerFilter === 'Collected') {
return token.ownerIsHolder;
}
// Filter out spam tokens
filtered = filtered.filter((token) => {
const isSpam = token.definition?.contract?.isSpam || token.isSpamByUser;
return !isSpam;
});

return token.ownerIsCreator;
});
// Filter by search query if present
if (searchCriteria.searchQuery) {
const searchQueryLower = searchCriteria.searchQuery.toLowerCase();
filtered = filtered.filter((token) =>
token.definition?.community?.name?.toLowerCase().includes(searchQueryLower)
);
}

// Filter by network if not 'All Networks'
if (searchCriteria.networkFilter !== 'All Networks') {
filtered = filtered.filter(
(token) => token.definition?.chain === searchCriteria.networkFilter
);
}

// Filter by owner status
if (searchCriteria.ownerFilter === 'Collected') {
filtered = filtered.filter((token) => token.ownerIsHolder);
} else if (searchCriteria.ownerFilter === 'Created') {
filtered = filtered.filter((token) => token.ownerIsCreator);
}

return filtered;
}, [
searchCriteria.networkFilter,
searchCriteria.ownerFilter,
Expand Down
10 changes: 7 additions & 3 deletions apps/mobile/src/utils/getChainIconComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import { OptimismIcon } from 'src/icons/OptimismIcon';
import { PoapIcon } from 'src/icons/PoapIcon';
import { PolygonIcon } from 'src/icons/PolygonIcon';
import { TezosIcon } from 'src/icons/TezosIcon';
import { WorldIcon } from 'src/icons/WorldIcon';
import { ZoraIcon } from 'src/icons/ZoraIcon';

import { ChainMetadata } from '~/shared/utils/chains';
import { Chain } from '~/shared/utils/chains';

export function getChainIconComponent(chain: Chain) {
switch (chain) {
case 'All Networks':
return <WorldIcon />;

export function getChainIconComponent(chain: ChainMetadata) {
switch (chain.name) {
case 'Ethereum':
return <EthIcon />;

Expand Down
14 changes: 14 additions & 0 deletions apps/web/public/icons/all_logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export function AddWalletSidebar({ handleRefresh, selectedChain, queryRef }: Pro
<VStack align="center" grow justify="center">
<EmptyState
title="It's looking empty"
description={`You do not have any ${selectedChain} pieces`}
description={`You do not have any ${
selectedChain === 'All Networks' ? 'pieces' : `${selectedChain} pieces`
}`}
>
<StyledButtonContainer>
<Button
Expand All @@ -68,7 +70,7 @@ export function AddWalletSidebar({ handleRefresh, selectedChain, queryRef }: Pro
variant="secondary"
onClick={handleManageWalletsClick}
>
{ctaButtonText}
{selectedChain === 'All Networks' ? 'Connect any account' : ctaButtonText}
</Button>
</StyledButtonContainer>
</EmptyState>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { RefreshIcon } from '~/icons/RefreshIcon';
import { contexts, flows } from '~/shared/analytics/constants';
import useExperience from '~/shared/hooks/useExperience';
import colors from '~/shared/theme/colors';
import { ChainMetadata, chainsMap } from '~/shared/utils/chains';
import { AvailableChains, ChainMetadata, chains, chainsMap } from '~/shared/utils/chains';
import { doesUserOwnWalletFromChainFamily } from '~/shared/utils/doesUserOwnWalletFromChainFamily';

import OnboardingDialog from '../GalleryOnboardingGuide/OnboardingDialog';
Expand Down Expand Up @@ -95,16 +95,15 @@ export function PiecesSidebar({ tokensRef, queryRef }: Props) {
rawTokensToDisplay: allTokens,
});

const [selectedChain, setSelectedChain] = useState<ChainMetadata>(chainsMap['Ethereum']);
const [selectedChain, setSelectedChain] = useState<ChainMetadata>(chainsMap['All Networks']);
const [selectedWallet, setSelectedWallet] = useState<SidebarWallet>('All');
const [selectedView, setSelectedView] = useState<TokenFilterType>('Collected');

const navbarHeight = useGlobalNavbarHeight();

const ownsWalletFromSelectedChainFamily = doesUserOwnWalletFromChainFamily(
selectedChain.name,
query
);
const ownsWalletFromSelectedChainFamily =
selectedChain.name === 'All Networks' ||
doesUserOwnWalletFromChainFamily(selectedChain.name, query);

const handleAddBlankBlockClick = useCallback(() => {
addWhitespace();
Expand All @@ -114,11 +113,19 @@ export function PiecesSidebar({ tokensRef, queryRef }: Props) {
// there's also wallet-specific token filtering happening in a child component, which should be lifted up here
const tokensToDisplay = useMemo(() => {
return tokenSearchResults.filter((token) => {
const isSpam =
token.isSpamByUser === null ? token.definition.contract?.isSpam : token.isSpamByUser;

// If we're searching, we want to search across all chains; the chain selector will be hidden during search
if (isSearching) {
return true;
}

// Check if network is 'All Networks', then return true for all tokens except spam
if (selectedChain.name === 'All Networks') {
return !isSpam;
}

if (token.definition.chain !== selectedChain.name) {
return false;
}
Expand All @@ -145,8 +152,6 @@ export function PiecesSidebar({ tokensRef, queryRef }: Props) {
}

// ...but incorporate with spam filtering logic for Collected view
const isSpam =
token.isSpamByUser !== null ? token.isSpamByUser : token.definition.contract?.isSpam;
if (selectedView === 'Hidden') {
return isSpam;
}
Expand All @@ -155,7 +160,7 @@ export function PiecesSidebar({ tokensRef, queryRef }: Props) {
}, [tokenSearchResults, isSearching, selectedChain, selectedView, selectedWallet]);

const isRefreshDisabledAtUserLevel = isRefreshDisabledForUser(query.viewer?.user?.dbid ?? '');
const refreshDisabled =
const shouldDisableRefresh =
isRefreshDisabledAtUserLevel ||
!doesUserOwnWalletFromChainFamily(selectedChain.name, query) ||
isLocked;
Expand Down Expand Up @@ -199,17 +204,26 @@ export function PiecesSidebar({ tokensRef, queryRef }: Props) {
setSelectedWallet('All');
}, []);

const availableChains = useMemo(() => {
return chains
.filter((chain) => chain.name !== 'All Networks')
.map((chain) => chain.name as AvailableChains);
}, []);

const handleRefresh = useCallback(async () => {
if (refreshDisabled) {
if (shouldDisableRefresh) {
return;
}

if (selectedView === 'Hidden') {
return;
}

await syncTokens({ type: selectedView, chain: selectedChain.name });
}, [refreshDisabled, selectedView, syncTokens, selectedChain.name]);
await syncTokens({
type: selectedView,
chain: selectedChain.name === 'All Networks' ? availableChains : selectedChain.name,
});
}, [shouldDisableRefresh, selectedView, syncTokens, selectedChain.name, availableChains]);

// Auto-sync tokens when the chain changes, and there are 0 tokens to display
useEffect(() => {
Expand Down Expand Up @@ -280,6 +294,7 @@ export function PiecesSidebar({ tokensRef, queryRef }: Props) {
isSearching={isSearching}
selectedView={selectedView}
onSelectedViewChange={handleSelectedViewChange}
isAllNetworksSelected={selectedChain.name === 'All Networks'}
/>
</Header>
<Header align="center" justify="space-between" gap={4}>
Expand Down Expand Up @@ -336,7 +351,7 @@ export function PiecesSidebar({ tokensRef, queryRef }: Props) {
eventContext={contexts.Editor}
onClick={handleRefresh}
variant="primary"
disabled={refreshDisabled}
disabled={shouldDisableRefresh}
>
<HStack gap={8} align="center">
{isLocked ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ type SidebarViewSelectorProps = {
isSearching: boolean;
selectedView: TokenFilterType;
onSelectedViewChange: (selectedView: TokenFilterType) => void;
isAllNetworksSelected: boolean;
};

export function SidebarViewSelector({
isSearching,
selectedView,
onSelectedViewChange,
isAllNetworksSelected,
}: SidebarViewSelectorProps) {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);

Expand Down Expand Up @@ -60,12 +62,14 @@ export function SidebarViewSelector({
name="Token Type"
eventContext={contexts.Editor}
label="Created"
disabled={isAllNetworksSelected}
/>
<DropdownItem
onClick={() => onSelectView('Hidden')}
name="Token Type"
eventContext={contexts.Editor}
label="Hidden"
disabled={isAllNetworksSelected}
/>
</DropdownSection>
</StyledDropdown>
Expand Down
Loading
Loading