Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
at main 252 lines 7.1 kB view raw
1import { useApolloClient } from "@apollo/client"; 2import { HEY_TREASURY, NATIVE_TOKEN_SYMBOL } from "@hey/data/constants"; 3import { 4 type AccountFragment, 5 type PostFragment, 6 type TippingAmountInput, 7 useBalancesBulkQuery, 8 useExecuteAccountActionMutation, 9 useExecutePostActionMutation 10} from "@hey/indexer"; 11import type { ApolloClientError } from "@hey/types/errors"; 12import type { ChangeEvent, RefObject } from "react"; 13import { memo, useCallback, useRef, useState } from "react"; 14import { toast } from "sonner"; 15import TopUpButton from "@/components/Shared/Account/TopUp/Button"; 16import LoginButton from "@/components/Shared/LoginButton"; 17import Skeleton from "@/components/Shared/Skeleton"; 18import { Button, Input, Spinner } from "@/components/Shared/UI"; 19import cn from "@/helpers/cn"; 20import errorToast from "@/helpers/errorToast"; 21import usePreventScrollOnNumberInput from "@/hooks/usePreventScrollOnNumberInput"; 22import useTransactionLifecycle from "@/hooks/useTransactionLifecycle"; 23import { useAccountStore } from "@/store/persisted/useAccountStore"; 24 25const submitButtonClassName = "w-full py-1.5 text-sm font-semibold"; 26 27interface TipMenuProps { 28 closePopover: () => void; 29 post?: PostFragment; 30 account?: AccountFragment; 31} 32 33const TipMenu = ({ closePopover, post, account }: TipMenuProps) => { 34 const { currentAccount } = useAccountStore(); 35 const [isSubmitting, setIsSubmitting] = useState(false); 36 const [amount, setAmount] = useState(1); 37 const [other, setOther] = useState(false); 38 const handleTransactionLifecycle = useTransactionLifecycle(); 39 const { cache } = useApolloClient(); 40 const inputRef = useRef<HTMLInputElement>(null); 41 usePreventScrollOnNumberInput(inputRef as RefObject<HTMLInputElement>); 42 43 const { data: balance, loading: balanceLoading } = useBalancesBulkQuery({ 44 fetchPolicy: "no-cache", 45 pollInterval: 3000, 46 skip: !currentAccount?.address, 47 variables: { 48 request: { address: currentAccount?.address, includeNative: true } 49 } 50 }); 51 52 const updateCache = () => { 53 if (post) { 54 if (!post.operations) { 55 return; 56 } 57 58 cache.modify({ 59 fields: { hasTipped: () => true }, 60 id: cache.identify(post.operations) 61 }); 62 cache.modify({ 63 fields: { 64 stats: (existingData) => ({ 65 ...existingData, 66 tips: existingData.tips + 1 67 }) 68 }, 69 id: cache.identify(post) 70 }); 71 } 72 }; 73 74 const onCompleted = () => { 75 setIsSubmitting(false); 76 closePopover(); 77 updateCache(); 78 toast.success(`Tipped ${amount} ${NATIVE_TOKEN_SYMBOL}`); 79 }; 80 81 const onError = useCallback((error: ApolloClientError) => { 82 setIsSubmitting(false); 83 errorToast(error); 84 }, []); 85 86 const cryptoRate = Number(amount); 87 const nativeBalance = 88 balance?.balancesBulk[0].__typename === "NativeAmount" 89 ? Number(balance.balancesBulk[0].value).toFixed(2) 90 : 0; 91 const canTip = Number(nativeBalance) >= cryptoRate; 92 93 const [executePostAction] = useExecutePostActionMutation({ 94 onCompleted: async ({ executePostAction }) => { 95 if (executePostAction.__typename === "ExecutePostActionResponse") { 96 return onCompleted(); 97 } 98 99 return await handleTransactionLifecycle({ 100 onCompleted, 101 onError, 102 transactionData: executePostAction 103 }); 104 }, 105 onError 106 }); 107 108 const [executeAccountAction] = useExecuteAccountActionMutation({ 109 onCompleted: async ({ executeAccountAction }) => { 110 if (executeAccountAction.__typename === "ExecuteAccountActionResponse") { 111 return onCompleted(); 112 } 113 114 return await handleTransactionLifecycle({ 115 onCompleted, 116 onError, 117 transactionData: executeAccountAction 118 }); 119 }, 120 onError 121 }); 122 123 const handleSetAmount = (amount: number) => { 124 setAmount(amount); 125 setOther(false); 126 }; 127 128 const onOtherAmount = (event: ChangeEvent<HTMLInputElement>) => { 129 const value = Number(event.target.value); 130 setAmount(value); 131 }; 132 133 const handleTip = async () => { 134 setIsSubmitting(true); 135 136 const tipping: TippingAmountInput = { 137 native: cryptoRate.toString(), 138 // 11 is a calculated value based on the referral pool of 20% and the Lens fee of 2.1% after the 1.5% lens fees cut 139 referrals: [{ address: HEY_TREASURY, percent: 11 }] 140 }; 141 142 if (post) { 143 return executePostAction({ 144 variables: { request: { action: { tipping }, post: post.id } } 145 }); 146 } 147 148 if (account) { 149 return executeAccountAction({ 150 variables: { 151 request: { account: account.address, action: { tipping } } 152 } 153 }); 154 } 155 }; 156 157 const amountDisabled = isSubmitting || !currentAccount; 158 159 if (!currentAccount) { 160 return <LoginButton className="m-5" title="Login to Tip" />; 161 } 162 163 return ( 164 <div className="m-5 space-y-3"> 165 <div className="space-y-2"> 166 <div className="flex items-center space-x-1 text-gray-500 text-xs dark:text-gray-200"> 167 <span>Balance:</span> 168 <span> 169 {nativeBalance ? ( 170 `${nativeBalance} ${NATIVE_TOKEN_SYMBOL}` 171 ) : ( 172 <Skeleton className="h-2.5 w-14 rounded-full" /> 173 )} 174 </span> 175 </div> 176 </div> 177 <div className="space-x-4"> 178 <Button 179 disabled={amountDisabled} 180 onClick={() => handleSetAmount(1)} 181 outline={amount !== 1} 182 size="sm" 183 > 184 $1 185 </Button> 186 <Button 187 disabled={amountDisabled} 188 onClick={() => handleSetAmount(2)} 189 outline={amount !== 2} 190 size="sm" 191 > 192 $2 193 </Button> 194 <Button 195 disabled={amountDisabled} 196 onClick={() => handleSetAmount(5)} 197 outline={amount !== 5} 198 size="sm" 199 > 200 $5 201 </Button> 202 <Button 203 disabled={amountDisabled} 204 onClick={() => { 205 handleSetAmount(other ? 1 : 10); 206 setOther(!other); 207 }} 208 outline={!other} 209 size="sm" 210 > 211 Other 212 </Button> 213 </div> 214 {other ? ( 215 <div> 216 <Input 217 className="no-spinner" 218 max={1000} 219 min={0} 220 onChange={onOtherAmount} 221 placeholder="300" 222 ref={inputRef} 223 type="number" 224 value={amount} 225 /> 226 </div> 227 ) : null} 228 {isSubmitting || balanceLoading ? ( 229 <Button 230 className={cn("flex justify-center", submitButtonClassName)} 231 disabled 232 icon={<Spinner className="my-0.5" size="xs" />} 233 /> 234 ) : canTip ? ( 235 <Button 236 className={submitButtonClassName} 237 disabled={!amount || isSubmitting || !canTip} 238 onClick={handleTip} 239 > 240 <b>Tip ${amount}</b> 241 </Button> 242 ) : ( 243 <TopUpButton 244 amountToTopUp={Math.ceil((amount - Number(nativeBalance)) * 20) / 20} 245 className="w-full" 246 /> 247 )} 248 </div> 249 ); 250}; 251 252export default memo(TipMenu);