diff --git a/types/api/universalProfile.ts b/types/api/universalProfile.ts new file mode 100644 index 0000000000..5173462685 --- /dev/null +++ b/types/api/universalProfile.ts @@ -0,0 +1,13 @@ +export type UPResponse = { + type: string; + hasProfileName: boolean; + hasProfileImage: boolean; + LSP3Profile: { + name: string; + profileImage: { + [key: number]: { + url: string; + }; + }; + }; +} diff --git a/ui/shared/entities/address/AddressEntity.tsx b/ui/shared/entities/address/AddressEntity.tsx index 482bcc3232..803b75b7f7 100644 --- a/ui/shared/entities/address/AddressEntity.tsx +++ b/ui/shared/entities/address/AddressEntity.tsx @@ -1,5 +1,6 @@ import type { As } from '@chakra-ui/react'; import { Box, Flex, Skeleton, Tooltip, chakra, VStack } from '@chakra-ui/react'; +import { useQueryClient } from '@tanstack/react-query'; import _omit from 'lodash/omit'; import React, { useEffect, useState } from 'react'; @@ -14,7 +15,7 @@ import * as EntityBase from 'ui/shared/entities/base/components'; import { getIconProps } from '../base/utils'; import AddressIdenticon from './AddressIdenticon'; -import makeUniversalProfileIdenticon from './IdenticonUniversalProfile'; +import { getUniversalProfile, IdenticonUniversalProfile } from './IdenticonUniversalProfileQuery'; if (process.browser) { import('@lukso/web-components/dist/components/lukso-profile'); } @@ -39,17 +40,6 @@ type IconProps = Pick { - const [ upUrl, setUpUrl ] = useState(''); - useEffect(() => { - (async() => { - const result = await makeUniversalProfileIdenticon(props.address.hash); - - setUpUrl(result); - - return; - })(); - }); - if (props.noIcon) { return null; } @@ -88,21 +78,7 @@ const Icon = (props: IconProps) => { ); } - if (upUrl !== '' && process.browser) { - console.log(`Generating profile for url ${ upUrl } and ${ props.address.hash }`); - return ( - - - - ); - } - - return ( + const contractIcon = ( { ); + + return ; } return ( @@ -130,6 +108,8 @@ const Icon = (props: IconProps) => { type ContentProps = Omit & Pick; const Content = chakra((props: ContentProps) => { + const queryClient = useQueryClient(); + const [ upName, setUpName ] = useState(''); if (props.address.name) { const label = ( @@ -146,11 +126,23 @@ const Content = chakra((props: ContentProps) => { ); } + useEffect(() => { // this causes a sort of loading state where the address suddenly switches to up name - needs fix? + (async() => { + const upData = await getUniversalProfile(props.address.hash, queryClient); + if (upData === undefined) { + return; + } + if (upData.LSP3Profile !== undefined) { + setUpName(upData.LSP3Profile.name); + } + })(); + }, [ props.address.hash, queryClient ]); + const displayedName = upName !== '' ? `@${ upName } (${ props.address.hash })` : props.address.hash; return ( ); }); diff --git a/ui/shared/entities/address/IdenticonUniversalProfile.tsx b/ui/shared/entities/address/IdenticonUniversalProfile.tsx deleted file mode 100644 index 2d9788f948..0000000000 --- a/ui/shared/entities/address/IdenticonUniversalProfile.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { getEnvValue } from 'configs/app/utils'; - -import config from '../../../../configs/app'; - -type UPResponse = { - type: string; - LSP3Profile: { - name: string; - profileImage: { - [key: number]: { - url: string; - }; - }; - }; -} - -const makeUniversalProfileIdenticon: (hash: string) => Promise = async(hash: string) => { - if (config.UI.views.address.identiconType !== 'universal_profile') { - return ''; - } - - const upApiUrl = getEnvValue('NEXT_PUBLIC_UP_API_URL') || ''; - const networkId = getEnvValue('NEXT_PUBLIC_NETWORK_ID') || '42'; - - const url = `${ upApiUrl }/v1/${ networkId }/address/${ hash }`; - - const response = await fetch(url); - if (!response.ok) { - return ''; - } - - const UPResult = (await response.json() as UPResponse); - const profilePictures = Object.values(UPResult.LSP3Profile.profileImage); - - // console.log(JSON.stringify(UPResult)); - // console.log(UPResult.type); - // console.log(JSON.stringify(UPResult.LSP3Profile)); - // console.log(JSON.stringify(UPResult.LSP3Profile.profileImage)); - // console.log(JSON.stringify(profilePictures)); - // console.log(profilePictures[4].url); - - return profilePictures[4].url; -}; - -export default makeUniversalProfileIdenticon; diff --git a/ui/shared/entities/address/IdenticonUniversalProfileQuery.tsx b/ui/shared/entities/address/IdenticonUniversalProfileQuery.tsx new file mode 100644 index 0000000000..3632818c61 --- /dev/null +++ b/ui/shared/entities/address/IdenticonUniversalProfileQuery.tsx @@ -0,0 +1,76 @@ +import { Box } from '@chakra-ui/react'; +import type { QueryClient } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; +import React, { useEffect, useState } from 'react'; + +import type { UPResponse } from '../../../../types/api/universalProfile'; + +import config from '../../../../configs/app'; +import { getEnvValue } from '../../../../configs/app/utils'; + +interface Props { + address: string; + fallbackIcon: JSX.Element; +} + +export const getUniversalProfile = async(address: string, queryClient: QueryClient) => { + if (config.UI.views.address.identiconType !== 'universal_profile') { + return undefined; + } + const query = queryClient.getQueryData([ 'universalProfile', { address: address } ]); + if (query !== undefined) { + return query; + } + + return await queryClient.fetchQuery({ + queryKey: [ 'universalProfile', { address: address } ], + queryFn: async() => { + const upApiUrl = getEnvValue('NEXT_PUBLIC_UP_API_URL') || ''; + const networkId = getEnvValue('NEXT_PUBLIC_NETWORK_ID') || '42'; + + const url = `${ upApiUrl }/v1/${ networkId }/address/${ address }`; + try { + const resp = await fetch(url); + const json = await resp.json(); + return json as UPResponse; + } catch (err) { + return undefined; + } + }, + }); +}; + +export const IdenticonUniversalProfile: React.FC = ({ address, fallbackIcon }) => { + const [ up, setUp ] = useState({} as UPResponse); + const queryClient = useQueryClient(); + useEffect(() => { + (async() => { + const upData = await getUniversalProfile(address, queryClient); + if (upData !== undefined) { + setUp(upData); + + return; + } + })(); + }, [ address, up, setUp, queryClient ]); + + if (up === undefined || up.LSP3Profile === undefined) { + return fallbackIcon; + } + + const lastImageIndex = Object.values(up.LSP3Profile.profileImage).length - 1; + + const profileImageUrl = up.hasProfileImage ? up.LSP3Profile.profileImage[lastImageIndex].url : ''; + return ( + + + + ); +}; + +export default IdenticonUniversalProfile; diff --git a/ui/shared/entities/address/global.d.ts b/ui/shared/entities/address/global.d.ts index e9b4c13ff5..de52a71ce8 100644 --- a/ui/shared/entities/address/global.d.ts +++ b/ui/shared/entities/address/global.d.ts @@ -44,8 +44,8 @@ declare global { } interface Window { - ethereum?: any; - lukso?: any; - grecaptcha?: any; + ethereum?: never; + lukso?: never; + grecaptcha?: never; } } diff --git a/ui/shared/entities/base/components.tsx b/ui/shared/entities/base/components.tsx index ce825d849a..a53a8447b0 100644 --- a/ui/shared/entities/base/components.tsx +++ b/ui/shared/entities/base/components.tsx @@ -64,7 +64,6 @@ const Link = chakra(({ isLoading, children, isExternal, onClick, href, noLink }: const Component = isExternal ? LinkExternal : LinkInternal; return ( - // @ts-ignore { + const queryClient = useQueryClient(); + const [ type, setType ] = useState(data.type); + useEffect(() => { // this causes a sort of loading state where the address suddenly switches to up name - needs fix? + (async() => { + const upData = await getUniversalProfile(data.address, queryClient); + if (upData === undefined) { + return; + } + if (upData.LSP3Profile !== undefined) { + setType('contract'); // when the type is contract the icon will know that it needs to get UP profile picture + } + })(); + }, [ data, queryClient, setType ]); + const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase(); const icon = ( ); const name = data.name && (