diff --git a/.million/store.json b/.million/store.json new file mode 100644 index 00000000..99d83d14 --- /dev/null +++ b/.million/store.json @@ -0,0 +1 @@ +{"encodings":[],"reactData":{},"unusedFiles":[],"mtime":null} \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 8bfd78f4..5003b415 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index e4c54b10..7d4ccbd2 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "preinstall": "npm_config_yes=true npm exec --no-package-lock=true only-allow@latest bun" }, "dependencies": { - "@airstack/airstack-react": "^0.6.4", "@million/lint": "^1.0.13", "@rainbow-me/rainbowkit": "^2.2.1", "@react-spring/web": "^9.7.5", diff --git a/src/api/airstack/followings.ts b/src/api/airstack/followings.ts new file mode 100644 index 00000000..42473f60 --- /dev/null +++ b/src/api/airstack/followings.ts @@ -0,0 +1,63 @@ +import type { Address } from 'viem' +import type { AirstackFollowingsResponse } from '#/types/requests' + +export const fetchAirstackFollowings = async ({ + profileAddress, + platform, + pageParam +}: { + profileAddress: Address + platform: string + pageParam?: string +}) => { + try { + const followingsQuery = ` + query FollowingsQuery ($platform: SocialDappName, $cursor: String) { + SocialFollowings( + input: {filter: {dappName: {_eq: $platform}, identity: {_eq: "${profileAddress}"}}, blockchain: ALL, limit: 200, cursor: $cursor} + ) { + Following { + followingAddress { + addresses + primaryDomain { + name + } + } + } + pageInfo { + nextCursor + hasPrevPage + hasNextPage + } + } + } + ` + + const response = await fetch(`https://api.airstack.xyz/gql`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: process.env.NEXT_PUBLIC_AIRSTACK_API_KEY + } as HeadersInit, + body: JSON.stringify({ + query: followingsQuery, + variables: { platform, cursor: pageParam }, + operationName: 'FollowingsQuery' + }) + }) + + const json = (await response.json()) as AirstackFollowingsResponse + return { + followings: json.data.SocialFollowings, + nextPageParam: json.data.SocialFollowings.pageInfo.nextCursor, + hasNextPage: json.data.SocialFollowings.pageInfo.hasNextPage, + hasPrevPage: json.data.SocialFollowings.pageInfo.hasPrevPage + } + } catch (error) { + return { + followings: null, + nextPageParam: undefined, + hasNextPage: false + } + } +} diff --git a/src/api/airstack/profile.ts b/src/api/airstack/profile.ts new file mode 100644 index 00000000..14b63268 --- /dev/null +++ b/src/api/airstack/profile.ts @@ -0,0 +1,37 @@ +import type { AirstackProfileResponse } from '#/types/requests' + +export const fetchAirstackProfile = async (platform: string, handle: string) => { + // Limit is set to 1 since we allow only full name search that returns only one profile + const profileQuery = ` + query ProfileQuery ($platform: SocialDappName) { + Socials( + input: {filter: {dappName: {_eq: $platform}, profileName: {_eq: "${handle.replace('@', '')}"}}, blockchain: ethereum, limit: 1} + ) { + Social { + profileImage + profileHandle + profileName + userAddress + } + } + } + ` + + const response = await fetch(`https://api.airstack.xyz/gql`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: process.env.NEXT_PUBLIC_AIRSTACK_API_KEY + } as HeadersInit, + body: JSON.stringify({ + query: profileQuery, + variables: { platform }, + operationName: 'ProfileQuery' + }) + }) + + const json = (await response.json()) as AirstackProfileResponse + + // return the first social profile since there is only one + return json.data.Socials.Social[0] +} diff --git a/src/app/cart/hooks/use-import-modal.ts b/src/app/cart/hooks/use-import-modal.ts index b07f5a74..be90c403 100644 --- a/src/app/cart/hooks/use-import-modal.ts +++ b/src/app/cart/hooks/use-import-modal.ts @@ -1,26 +1,27 @@ import type { Address } from 'viem' import { useEffect, useMemo, useState } from 'react' -import { init, useQuery, useQueryWithPagination } from '@airstack/airstack-react' +import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import { SECOND } from '#/lib/constants' import { useCart } from '#/contexts/cart-context' import { listOpAddListRecord } from '#/utils/list-ops' import type { ImportPlatformType } from '#/types/common' +import type { AirstackFollowings } from '#/types/requests' +import { fetchAirstackProfile } from '#/api/airstack/profile' import { useEFPProfile } from '#/contexts/efp-profile-context' +import { fetchAirstackFollowings } from '#/api/airstack/followings' -init('0366bbe276e04996af5f92ebb7899f19', { env: 'dev', cache: true }) - -export type importFollowingType = { +export type ImportFollowingType = { address: Address - domains: { name: string }[] + primaryDomain: string } const useImportModal = (platform: ImportPlatformType) => { const [handle, setHandle] = useState('') const [currHandle, setCurrHandle] = useState('') - const [followings, setFollowings] = useState([]) - const [allFollowings, setAllFollowings] = useState([]) + const [followings, setFollowings] = useState([]) + const [allFollowings, setAllFollowings] = useState([]) const [onlyImportWithEns, setOnlyImportWithEns] = useState(true) const [isFollowingsLoading, setIsFollowingsLoading] = useState(false) @@ -36,96 +37,78 @@ const useImportModal = (platform: ImportPlatformType) => { return () => clearTimeout(inputTimeout) }, [currHandle]) - // Fetch profile from Airstack - const profileQuery = ` - query ProfileQuery ($platform: SocialDappName) { - Socials( - input: {filter: {dappName: {_eq: $platform}, profileName: {_eq: "${handle.replace('@', '')}"}}, blockchain: ethereum, limit: 1} - ) { - Social { - profileImage - profileHandle - profileName - userAddress - } - } - } - ` - - const { data: fetchedProfile, loading: isSocialProfileLoading } = useQuery(profileQuery, { - platform + const { data: fetchedProfile, isFetching: isSocialProfileLoading } = useQuery({ + queryKey: ['profile', platform, handle], + queryFn: async () => await fetchAirstackProfile(platform, handle), + enabled: !!handle }) - const socialProfile = - fetchedProfile && !!fetchedProfile?.Socials?.Social - ? { - ...fetchedProfile?.Socials?.Social?.[0], - profileImage: fetchedProfile?.Socials?.Social?.[0]?.profileImage?.includes('ipfs://') - ? `https://gateway.pinata.cloud/ipfs/${fetchedProfile?.Socials?.Social?.[0]?.profileImage.replace( - 'ipfs://', - '' - )}` - : fetchedProfile?.Socials?.Social?.[0]?.profileImage - } - : null - - // Fetch followings from Airstack - const followingsQuery = useMemo( - () => ` - query FollowingsQuery ($platform: SocialDappName) { - SocialFollowings( - input: {filter: {dappName: {_eq: $platform}, identity: {_eq: "${socialProfile?.userAddress}"}}, blockchain: ALL, limit: 200} - ) { - Following { - followingAddress { - addresses - primaryDomain { - name - } - } + + // replace ipfs with pinata gateway (pinata currently most stable for me https://ipfs.github.io/public-gateway-checker/) + const socialProfile = fetchedProfile + ? { + ...fetchedProfile, + profileImage: fetchedProfile?.profileImage?.includes('ipfs://') + ? `https://ipfs.io/ipfs/${fetchedProfile?.profileImage.replace('ipfs://', '')}` + : fetchedProfile?.profileImage } - } - } -`, - [socialProfile] - ) + : null const { data: fetchedFollowings, - loading: isFetchedFollowingsLoading, - pagination: { hasNextPage, getNextPage, hasPrevPage } - } = useQueryWithPagination(followingsQuery, { platform }) + isLoading: isFetchedFollowingsLoading, + hasNextPage: hasNextPageFollowings, + hasPreviousPage: hasPreviousPageFollowings, + fetchNextPage: fetchNextPageFollowings + } = useInfiniteQuery({ + queryKey: ['followings', platform, handle, socialProfile?.userAddress], + queryFn: async ({ pageParam }) => { + if (!socialProfile?.userAddress) + return { followings: null, nextPageParam: undefined, hasNextPage: false } + + return await fetchAirstackFollowings({ + profileAddress: socialProfile.userAddress as Address, + platform, + pageParam + }) + }, + initialPageParam: '', + getNextPageParam: lastPage => (lastPage?.hasNextPage ? lastPage?.nextPageParam : undefined) + }) + + const reducedFollowings = useMemo( + () => + fetchedFollowings?.pages.reduce((acc, page) => { + if (page?.followings?.Following) acc.push(...page.followings.Following) + return acc + }, [] as AirstackFollowings[]), + [fetchedFollowings] + ) useEffect(() => { - if (currHandle !== handle) return - if (!hasPrevPage) setFollowings([]) - if (hasNextPage) getNextPage() - - if ( - fetchedFollowings?.SocialFollowings?.Following && - fetchedFollowings?.SocialFollowings?.Following.length > 0 - ) { + if (currHandle !== handle || isFetchedFollowingsLoading) return + if (!hasPreviousPageFollowings) setFollowings([]) + + if (reducedFollowings && reducedFollowings.length > 0) { + if (hasNextPageFollowings) fetchNextPageFollowings() setIsFollowingsLoading(true) - const newFollowingAddresses = fetchedFollowings?.SocialFollowings?.Following.map( - (following: any) => ({ - address: following.followingAddress.addresses?.[0], - primaryDomain: following.followingAddress?.primaryDomain?.name - }) - ) + const followingAddresses = reducedFollowings.map((following: any) => ({ + address: following.followingAddress.addresses?.[0], + primaryDomain: following.followingAddress?.primaryDomain?.name + })) - const filteredNewFollowingAddresses = newFollowingAddresses.filter((following: any) => + const filteredFollowingAddresses = followingAddresses.filter((following: any) => onlyImportWithEns ? !!following.primaryDomain : true ) - setAllFollowings(currFollowings => [ - ...new Set([...currFollowings, ...newFollowingAddresses]) - ]) - setFollowings(currFollowings => [ - ...new Set([...currFollowings, ...filteredNewFollowingAddresses]) - ]) + if (!hasNextPageFollowings) { + setAllFollowings(followingAddresses) + setFollowings(filteredFollowingAddresses) + } } - if (!hasNextPage) setIsFollowingsLoading(false) - }, [fetchedFollowings]) + + if (!hasNextPageFollowings) setIsFollowingsLoading(false) + }, [reducedFollowings]) useEffect(() => { if (!allFollowings || allFollowings.length === 0) return diff --git a/src/app/leaderboard/components/filters.tsx b/src/app/leaderboard/components/filters.tsx index 6849a8f4..f0e8d1e2 100644 --- a/src/app/leaderboard/components/filters.tsx +++ b/src/app/leaderboard/components/filters.tsx @@ -23,7 +23,7 @@ const Filters: React.FC = ({ filter, onSelectFilter }) => {
setIsDropdownOpen(prev => !prev)} - className='flex w-full cursor-pointer flex-wrap h-[50px] z-30 justify-between px-3 glass-card border-grey hover:border-text/80 transition-colors rounded-xl border-[3px] bg-neutral items-center gap-4' + className='flex w-full cursor-pointer flex-wrap h-[50px] z-30 justify-between px-3 border-grey hover:border-text/80 transition-colors rounded-xl border-[3px] bg-neutral items-center gap-4' >
= ({ filter, onSelectFilter }) => {
diff --git a/src/app/leaderboard/components/table.tsx b/src/app/leaderboard/components/table.tsx index fdf70313..2b51988a 100644 --- a/src/app/leaderboard/components/table.tsx +++ b/src/app/leaderboard/components/table.tsx @@ -99,7 +99,7 @@ const LeaderboardTable = () => {
-
+
diff --git a/src/components/checkout/select-chain-card.tsx b/src/components/checkout/select-chain-card.tsx index fa050c6e..93b7e025 100644 --- a/src/components/checkout/select-chain-card.tsx +++ b/src/components/checkout/select-chain-card.tsx @@ -30,12 +30,11 @@ export function SelectChainCard({ setNewListAsPrimary: boolean setSetNewListAsPrimary: (state: boolean) => void }) { + const { t } = useTranslation() + const { lists } = useEFPProfile() const currentChainId = useChainId() const { switchChain } = useSwitchChain() - const { lists } = useEFPProfile() - const { t } = useTranslation() - return ( <>
diff --git a/src/lib/wagmi.ts b/src/lib/wagmi.ts index 5e03ed85..fb0d5c34 100644 --- a/src/lib/wagmi.ts +++ b/src/lib/wagmi.ts @@ -15,7 +15,7 @@ import { http, fallback, createStorage, cookieStorage, createConfig } from 'wagm import { APP_DESCRIPTION, APP_NAME, APP_URL } from '#/lib/constants' -coinbaseWallet.preference = 'smartWalletOnly' +coinbaseWallet.preference = 'all' // Define the connectors for the app // Purposely using only these for now because of a localStorage error with the Coinbase Wallet connector diff --git a/src/types/requests.ts b/src/types/requests.ts index cffdee02..74e35402 100644 --- a/src/types/requests.ts +++ b/src/types/requests.ts @@ -267,3 +267,30 @@ export type RecommendedProfilesResponseType = { } export type QRCodeResponse = StaticImageData + +// Airstack +export type AirstackProfileResponse = { + data: { + Socials: { + Social: { + profileImage: string + profileHandle: string + profileName: string + userAddress: string + }[] + } + } +} + +export type AirstackFollowings = { + followingAddress: { addresses: Address[]; primaryDomain: { name: string } } +} + +export type AirstackFollowingsResponse = { + data: { + SocialFollowings: { + Following: AirstackFollowings[] + pageInfo: { nextCursor: string; hasPrevPage: boolean; hasNextPage: boolean } + } + } +}