Skip to content

Commit

Permalink
feat: nft avatar modal added (#3536)
Browse files Browse the repository at this point in the history
* feat: nft avatar modal added

* nft avatar modal loading state changes

* used nft gallery instead of nft feed component

* addressed review comments

* addressed review comments

---------

Co-authored-by: bigint <bigint@lenster.xyz>
  • Loading branch information
bhavya2611 and bigint committed Aug 17, 2023
1 parent b0a27a7 commit 684ca93
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 264 deletions.
55 changes: 24 additions & 31 deletions apps/web/src/components/Nft/SingleNft.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { RARIBLE_URL, STATIC_IMAGES_URL } from '@lenster/data/constants';
import type { Nft } from '@lenster/lens';
import sanitizeDStorageUrl from '@lenster/lib/sanitizeDStorageUrl';
import { Card } from '@lenster/ui';
import Link from 'next/link';
import type { FC } from 'react';
import { CHAIN_ID } from 'src/constants';

Expand All @@ -20,10 +19,13 @@ const SingleNft: FC<SingleNftProps> = ({ nft, linkToDetail = true }) => {

return (
<Card>
{nft?.originalContent?.animatedUrl ? (
<div className="divider h-52 sm:h-80 sm:rounded-t-[10px]">
{nft?.originalContent?.animatedUrl?.includes('.gltf') ? (
<Link href={nftURL ?? ''} target="_blank" rel="noreferrer noopener">
<div
onClick={() => nftURL && window.open(nftURL, '_blank')}
className="cursor-pointer"
>
{nft?.originalContent?.animatedUrl ? (
<div className="divider h-52 sm:h-80 sm:rounded-t-[10px]">
{nft?.originalContent?.animatedUrl?.includes('.gltf') ? (
<div
style={{
backgroundImage: `url(${`${STATIC_IMAGES_URL}/placeholder.webp`})`,
Expand All @@ -32,17 +34,15 @@ const SingleNft: FC<SingleNftProps> = ({ nft, linkToDetail = true }) => {
backgroundRepeat: 'no-repeat'
}}
/>
</Link>
) : (
<iframe
title={`${nft.contractAddress}:${nft.tokenId}`}
sandbox=""
src={sanitizeDStorageUrl(nft?.originalContent?.animatedUrl)}
/>
)}
</div>
) : (
<Link href={nftURL ?? ''} target="_blank" rel="noreferrer noopener">
) : (
<iframe
title={`${nft.contractAddress}:${nft.tokenId}`}
sandbox=""
src={sanitizeDStorageUrl(nft?.originalContent?.animatedUrl)}
/>
)}
</div>
) : (
<div
className="divider h-52 sm:h-80 sm:rounded-t-[10px]"
style={{
Expand All @@ -56,23 +56,16 @@ const SingleNft: FC<SingleNftProps> = ({ nft, linkToDetail = true }) => {
backgroundRepeat: 'no-repeat'
}}
/>
</Link>
)}
<div className="space-y-1 p-5">
{nft.collectionName && (
<div className="lt-text-gray-500 truncate text-sm">
{nft.collectionName}
</div>
)}
<div className="truncate">
<Link
className="font-bold"
href={nftURL ?? ''}
target="_blank"
rel="noreferrer noopener"
>
<div className="space-y-1 p-5">
{nft.collectionName && (
<div className="lt-text-gray-500 truncate text-sm">
{nft.collectionName}
</div>
)}
<div className="truncate">
{nft.name ? nft.name : `#${nft.tokenId}`}
</Link>
</div>
</div>
</div>
</Card>
Expand Down
23 changes: 20 additions & 3 deletions apps/web/src/components/Profile/NftGallery/Picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import type { NftGalleryItem } from 'src/store/nft-gallery';
import { useNftGalleryStore } from 'src/store/nft-gallery';
import { mainnet } from 'wagmi/chains';

const Picker: FC = () => {
interface PickerProps {
onlyAllowOne?: boolean;
}

const Picker: FC<PickerProps> = ({ onlyAllowOne }) => {
const currentProfile = useAppStore((state) => state.currentProfile);
const gallery = useNftGalleryStore((state) => state.gallery);
const setGallery = useNftGalleryStore((state) => state.setGallery);
Expand Down Expand Up @@ -80,11 +84,24 @@ const Picker: FC = () => {
if (gallery.items.length === 50) {
return toast.error(t`Only 50 items allowed for gallery`);
}

const customId = `${item.chainId}_${item.contractAddress}_${item.tokenId}`;
const nft = {
itemId: customId,
...item
};

if (onlyAllowOne) {
setGallery({
...gallery,
name: '',
items: [nft],
toAdd: [],
toRemove: []
});
return;
}

const alreadySelectedIndex = gallery.items.findIndex(
(n) => n.itemId === customId
);
Expand Down Expand Up @@ -143,7 +160,7 @@ const Picker: FC = () => {
});

return (
<div className="grid grid-cols-1 gap-4 p-5 sm:grid-cols-3">
<div className="grid grid-cols-1 gap-4 p-5 sm:grid-cols-3 md:grid-cols-4">
{nfts?.map((nft, index) => {
const id = `${nft.chainId}_${nft.contractAddress}_${nft.tokenId}`;
const isSelected = selectedItems.includes(id);
Expand All @@ -156,7 +173,7 @@ const Picker: FC = () => {
)}
>
{isSelected && (
<button className="bg-brand-500 absolute right-2 top-2 rounded-full">
<button className="bg-brand-500 absolute right-2 top-2 z-20 rounded-full">
<CheckIcon className="h-5 w-5 p-1 text-white" />
</button>
)}
Expand Down
208 changes: 208 additions & 0 deletions apps/web/src/components/Settings/Profile/NftAvatarModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import Picker from '@components/Profile/NftGallery/Picker';
import { PencilIcon } from '@heroicons/react/outline';
import { LensHub } from '@lenster/abis';
import { LENSHUB_PROXY } from '@lenster/data/constants';
import { Errors } from '@lenster/data/errors';
import { SETTINGS } from '@lenster/data/tracking';
import {
type UpdateProfileImageRequest,
useBroadcastMutation,
useCreateSetProfileImageUriTypedDataMutation,
useCreateSetProfileImageUriViaDispatcherMutation,
useNftChallengeLazyQuery
} from '@lenster/lens';
import getSignature from '@lenster/lib/getSignature';
import { Button, ErrorMessage, Modal, Spinner } from '@lenster/ui';
import errorToast from '@lib/errorToast';
import { Leafwatch } from '@lib/leafwatch';
import { t, Trans } from '@lingui/macro';
import type { Dispatch, FC, SetStateAction } from 'react';
import { useState } from 'react';
import toast from 'react-hot-toast';
import { useAppStore } from 'src/store/app';
import { useNftGalleryStore } from 'src/store/nft-gallery';
import { useNonceStore } from 'src/store/nonce';
import { useContractWrite, useSignMessage, useSignTypedData } from 'wagmi';

interface NftAvatarModalProps {
showNftAvatarModal: boolean;
setShowNftAvatarModal: Dispatch<SetStateAction<boolean>>;
}

const NftAvatarModal: FC<NftAvatarModalProps> = ({
showNftAvatarModal,
setShowNftAvatarModal
}) => {
const userSigNonce = useNonceStore((state) => state.userSigNonce);
const [isLoading, setIsLoading] = useState(false);
const currentProfile = useAppStore((state) => state.currentProfile);
const gallery = useNftGalleryStore((state) => state.gallery);

const { signMessageAsync } = useSignMessage();

const setUserSigNonce = useNonceStore((state) => state.setUserSigNonce);

const onCompleted = (__typename?: 'RelayError' | 'RelayerResult') => {
if (__typename === 'RelayError') {
return;
}

setIsLoading(false);
toast.success(t`Avatar updated successfully!`);
Leafwatch.track(SETTINGS.PROFILE.SET_NFT_PICTURE);
};

const onError = (error: any) => {
setIsLoading(false);
errorToast(error);
};

const [loadChallenge] = useNftChallengeLazyQuery();
const [broadcast] = useBroadcastMutation({
onCompleted: ({ broadcast }) => onCompleted(broadcast.__typename)
});
const { signTypedDataAsync } = useSignTypedData({ onError });

const { error, write } = useContractWrite({
address: LENSHUB_PROXY,
abi: LensHub,
functionName: 'setProfileImageURI',
onSuccess: () => onCompleted(),
onError
});

// Dispatcher
const canUseRelay = currentProfile?.dispatcher?.canUseRelay;
const isSponsored = currentProfile?.dispatcher?.sponsor;

const [createSetProfileImageURITypedData] =
useCreateSetProfileImageUriTypedDataMutation({
onCompleted: async ({ createSetProfileImageURITypedData }) => {
const { id, typedData } = createSetProfileImageURITypedData;
const signature = await signTypedDataAsync(getSignature(typedData));
setUserSigNonce(userSigNonce + 1);
const { data } = await broadcast({
variables: { request: { id, signature } }
});
if (data?.broadcast.__typename === 'RelayError') {
const { profileId, imageURI } = typedData.value;
return write?.({ args: [profileId, imageURI] });
}
},
onError
});

const [createSetProfileImageURIViaDispatcher] =
useCreateSetProfileImageUriViaDispatcherMutation({
onCompleted: ({ createSetProfileImageURIViaDispatcher }) =>
onCompleted(createSetProfileImageURIViaDispatcher.__typename),
onError
});

const createViaDispatcher = async (request: UpdateProfileImageRequest) => {
const { data } = await createSetProfileImageURIViaDispatcher({
variables: { request }
});
if (
data?.createSetProfileImageURIViaDispatcher?.__typename === 'RelayError'
) {
return await createSetProfileImageURITypedData({
variables: {
options: { overrideSigNonce: userSigNonce },
request
}
});
}
};

const setAvatar = async () => {
if (!currentProfile || gallery.items.length === 0) {
return toast.error(Errors.SignWallet);
}

try {
setIsLoading(true);
const { contractAddress, tokenId, chainId } = gallery.items[0];
const challengeRes = await loadChallenge({
variables: {
request: {
ethereumAddress: currentProfile?.ownedBy,
nfts: [
{
contractAddress,
tokenId,
chainId
}
]
}
}
});

const signature = await signMessageAsync({
message: challengeRes?.data?.nftOwnershipChallenge?.text as string
});

const request: UpdateProfileImageRequest = {
profileId: currentProfile?.id,
nftData: {
id: challengeRes?.data?.nftOwnershipChallenge?.id,
signature
}
};

setShowNftAvatarModal(false);

if (canUseRelay && isSponsored) {
return await createViaDispatcher(request);
}

return await createSetProfileImageURITypedData({
variables: {
options: { overrideSigNonce: userSigNonce },
request
}
});
} catch (error) {
onError(error);
}
};

return (
<Modal
size="lg"
title={t`Select a NFT`}
show={showNftAvatarModal}
onClose={() => setShowNftAvatarModal(false)}
>
<div className="flex flex-col">
{error && (
<ErrorMessage
className="mb-3"
title={t`Transaction failed!`}
error={error}
/>
)}
<div className="mb-4 mr-1 flex h-[70vh] overflow-y-scroll">
<Picker onlyAllowOne={true} />
</div>
<div className="ml-auto flex items-center space-x-2 p-4">
<Button
onClick={setAvatar}
disabled={isLoading || gallery.items.length === 0}
icon={
isLoading ? (
<Spinner size="xs" />
) : (
<PencilIcon className="h-4 w-4" />
)
}
>
<Trans>Save</Trans>
</Button>
</div>
</div>
</Modal>
);
};

export default NftAvatarModal;
Loading

2 comments on commit 684ca93

@vercel
Copy link

@vercel vercel bot commented on 684ca93 Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

prerender – ./apps/prerender

prerender-git-main-lenster.vercel.app
prerender-lenster.vercel.app
prerender.lenster.xyz

@vercel
Copy link

@vercel vercel bot commented on 684ca93 Aug 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

web – ./apps/web

web-git-main-lenster.vercel.app
lenster.xyz
lenster.vercel.app
web-lenster.vercel.app

Please sign in to comment.