idempotent zaps
This commit is contained in:
		
							parent
							
								
									374a7985da
								
							
						
					
					
						commit
						73ad93f2bb
					
				| @ -786,6 +786,52 @@ export default { | ||||
| 
 | ||||
|       notifyZapped({ models, id }) | ||||
| 
 | ||||
|       return { | ||||
|         id, | ||||
|         sats, | ||||
|         act, | ||||
|         path: item.path | ||||
|       } | ||||
|     }, | ||||
|     idempotentAct: async (parent, { id, sats, act = 'TIP', hash, hmac }, { me, models, lnd, headers }) => { | ||||
|       if (!me) { | ||||
|         throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } }) | ||||
|       } | ||||
| 
 | ||||
|       await ssValidate(actSchema, { sats, act }) | ||||
|       await assertGofacYourself({ models, headers }) | ||||
| 
 | ||||
|       const [item] = await models.$queryRawUnsafe(` | ||||
|       ${SELECT} | ||||
|       FROM "Item" | ||||
|       WHERE id = $1`, Number(id))
 | ||||
| 
 | ||||
|       if (Number(item.userId) === Number(me.id)) { | ||||
|         throw new GraphQLError('cannot zap your self', { extensions: { code: 'BAD_INPUT' } }) | ||||
|       } | ||||
| 
 | ||||
|       // Disallow tips if me is one of the forward user recipients
 | ||||
|       if (act === 'TIP') { | ||||
|         const existingForwards = await models.itemForward.findMany({ where: { itemId: Number(id) } }) | ||||
|         if (existingForwards.some(fwd => Number(fwd.userId) === Number(me.id))) { | ||||
|           throw new GraphQLError('cannot zap a post for which you are forwarded zaps', { extensions: { code: 'BAD_INPUT' } }) | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       await serializeInvoicable( | ||||
|         models.$queryRaw` | ||||
|         SELECT | ||||
|           item_act(${Number(id)}::INTEGER, ${me.id}::INTEGER, ${act}::"ItemActType", | ||||
|           (SELECT ${Number(sats)}::INTEGER - COALESCE(sum(msats) / 1000, 0) | ||||
|            FROM "ItemAct" | ||||
|            WHERE act IN ('TIP', 'FEE') | ||||
|            AND "itemId" = ${Number(id)}::INTEGER | ||||
|            AND "userId" = ${me.id}::INTEGER)::INTEGER)`,
 | ||||
|         { me, models, lnd, hash, hmac, enforceFee: sats } | ||||
|       ) | ||||
| 
 | ||||
|       notifyZapped({ models, id }) | ||||
| 
 | ||||
|       return { | ||||
|         id, | ||||
|         sats, | ||||
|  | ||||
| @ -37,6 +37,7 @@ export default gql` | ||||
|     updateNoteId(id: ID!, noteId: String!): Item! | ||||
|     upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: String): Item! | ||||
|     act(id: ID!, sats: Int, act: String, hash: String, hmac: String): ItemActResult! | ||||
|     idempotentAct(id: ID!, sats: Int, hash: String, hmac: String): ItemActResult! | ||||
|     pollVote(id: ID!, hash: String, hmac: String): ID! | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -108,7 +108,6 @@ export default function Comment ({ | ||||
|   const ref = useRef(null) | ||||
|   const router = useRouter() | ||||
|   const root = useRoot() | ||||
|   const [pendingSats, setPendingSats] = useState(0) | ||||
|   const { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text }) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
| @ -148,7 +147,7 @@ export default function Comment ({ | ||||
|       <div className={`${itemStyles.item} ${styles.item}`}> | ||||
|         {item.meDontLikeSats > item.meSats | ||||
|           ? <DownZap width={24} height={24} className={styles.dontLike} id={item.id} meDontLikeSats={item.meDontLikeSats} /> | ||||
|           : <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />} | ||||
|           : <UpVote item={item} className={styles.upvote} />} | ||||
|         <div className={`${itemStyles.hunk} ${styles.hunk}`}> | ||||
|           <div className='d-flex align-items-center'> | ||||
|             {item.user?.meMute && !includeParent && collapse === 'yep' | ||||
| @ -162,7 +161,6 @@ export default function Comment ({ | ||||
|                 </span>) | ||||
|               : <ItemInfo | ||||
|                   item={item} | ||||
|                   pendingSats={pendingSats} | ||||
|                   commentsText='replies' | ||||
|                   commentTextSingular='reply' | ||||
|                   className={`${itemStyles.other} ${styles.other}`} | ||||
|  | ||||
| @ -6,7 +6,6 @@ import AccordianItem from './accordian-item' | ||||
| import Flag from '../svgs/flag-fill.svg' | ||||
| import { useMemo } from 'react' | ||||
| import getColor from '../lib/rainbow' | ||||
| import { useLightning } from './lightning' | ||||
| 
 | ||||
| export function DownZap ({ id, meDontLikeSats, ...props }) { | ||||
|   const style = useMemo(() => (meDontLikeSats | ||||
| @ -23,7 +22,6 @@ export function DownZap ({ id, meDontLikeSats, ...props }) { | ||||
| function DownZapper ({ id, As, children }) { | ||||
|   const toaster = useToast() | ||||
|   const showModal = useShowModal() | ||||
|   const strike = useLightning() | ||||
| 
 | ||||
|   return ( | ||||
|     <As | ||||
| @ -34,7 +32,7 @@ function DownZapper ({ id, As, children }) { | ||||
|               onClose={() => { | ||||
|                 onClose() | ||||
|                 toaster.success('item downzapped') | ||||
|               }} itemId={id} strike={strike} down | ||||
|               }} itemId={id} down | ||||
|             > | ||||
|               <AccordianItem | ||||
|                 header='what is a downzap?' body={ | ||||
|  | ||||
| @ -6,6 +6,9 @@ import { useMe } from './me' | ||||
| import UpBolt from '../svgs/bolt.svg' | ||||
| import { amountSchema } from '../lib/validate' | ||||
| import { gql, useMutation } from '@apollo/client' | ||||
| import { payOrLoginError, useInvoiceModal } from './invoice' | ||||
| import { useToast } from './toast' | ||||
| import { useLightning } from './lightning' | ||||
| 
 | ||||
| const defaultTips = [100, 1000, 10000, 100000] | ||||
| 
 | ||||
| @ -37,10 +40,11 @@ const addCustomTip = (amount) => { | ||||
|   window.localStorage.setItem('custom-tips', JSON.stringify(customTips)) | ||||
| } | ||||
| 
 | ||||
| export default function ItemAct ({ onClose, itemId, down, strike, children }) { | ||||
| export default function ItemAct ({ onClose, itemId, down, children }) { | ||||
|   const inputRef = useRef(null) | ||||
|   const me = useMe() | ||||
|   const [oValue, setOValue] = useState() | ||||
|   const strike = useLightning() | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     inputRef.current?.focus() | ||||
| @ -63,7 +67,7 @@ export default function ItemAct ({ onClose, itemId, down, strike, children }) { | ||||
|         hmac | ||||
|       } | ||||
|     }) | ||||
|     strike && await strike() | ||||
|     await strike() | ||||
|     addCustomTip(Number(amount)) | ||||
|     onClose() | ||||
|   }, [act, down, itemId, strike]) | ||||
| @ -166,3 +170,110 @@ export function useAct ({ onUpdate } = {}) { | ||||
|       }`, { update }
 | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export function useZap () { | ||||
|   const update = useCallback((cache, args) => { | ||||
|     const { data: { idempotentAct: { id, sats, path } } } = args | ||||
| 
 | ||||
|     // determine how much we increased existing sats by by checking the
 | ||||
|     // difference between result sats and meSats
 | ||||
|     // if it's negative, skip the cache as it's an out of order update
 | ||||
|     // if it's positive, add it to sats and commentSats
 | ||||
| 
 | ||||
|     const item = cache.readFragment({ | ||||
|       id: `Item:${id}`, | ||||
|       fragment: gql` | ||||
|         fragment ItemMeSats on Item { | ||||
|           meSats | ||||
|         } | ||||
|       ` | ||||
|     }) | ||||
| 
 | ||||
|     const satsDelta = sats - item.meSats | ||||
| 
 | ||||
|     if (satsDelta > 0) { | ||||
|       cache.modify({ | ||||
|         id: `Item:${id}`, | ||||
|         fields: { | ||||
|           sats (existingSats = 0) { | ||||
|             return existingSats + satsDelta | ||||
|           }, | ||||
|           meSats: () => { | ||||
|             return sats | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
| 
 | ||||
|       // update all ancestors
 | ||||
|       path.split('.').forEach(aId => { | ||||
|         if (Number(aId) === Number(id)) return | ||||
|         cache.modify({ | ||||
|           id: `Item:${aId}`, | ||||
|           fields: { | ||||
|             commentSats (existingCommentSats = 0) { | ||||
|               return existingCommentSats + satsDelta | ||||
|             } | ||||
|           } | ||||
|         }) | ||||
|       }) | ||||
|     } | ||||
|   }, []) | ||||
| 
 | ||||
|   const [zap] = useMutation( | ||||
|     gql` | ||||
|       mutation idempotentAct($id: ID!, $sats: Int!, $hash: String, $hmac: String) { | ||||
|         idempotentAct(id: $id, sats: $sats, hash: $hash, hmac: $hmac) { | ||||
|           id | ||||
|           sats | ||||
|           path | ||||
|         } | ||||
|       }`, { update }
 | ||||
|   ) | ||||
| 
 | ||||
|   const toaster = useToast() | ||||
|   const strike = useLightning() | ||||
|   const [act] = useAct() | ||||
| 
 | ||||
|   const showInvoiceModal = useInvoiceModal( | ||||
|     async ({ hash, hmac }, { variables }) => { | ||||
|       await act({ variables: { ...variables, hash, hmac } }) | ||||
|       strike() | ||||
|     }, [act, strike]) | ||||
| 
 | ||||
|   return useCallback(async ({ item, me }) => { | ||||
|     console.log(item) | ||||
|     const meSats = (item?.meSats || 0) | ||||
| 
 | ||||
|     // what should our next tip be?
 | ||||
|     let sats = me?.privates?.tipDefault || 1 | ||||
|     if (me?.privates?.turboTipping) { | ||||
|       while (meSats >= sats) { | ||||
|         sats *= 10 | ||||
|       } | ||||
|     } else { | ||||
|       sats = meSats + sats | ||||
|     } | ||||
| 
 | ||||
|     const variables = { id: item.id, sats, act: 'TIP' } | ||||
|     try { | ||||
|       await zap({ | ||||
|         variables, | ||||
|         optimisticResponse: { | ||||
|           idempotentAct: { | ||||
|             path: item.path, | ||||
|             ...variables | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     } catch (error) { | ||||
|       if (payOrLoginError(error)) { | ||||
|         // call non-idempotent version
 | ||||
|         const amount = sats - meSats | ||||
|         showInvoiceModal({ amount }, { variables: { ...variables, sats: amount } }) | ||||
|         return | ||||
|       } | ||||
|       console.error(error) | ||||
|       toaster.danger(error?.message || error?.toString?.()) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|  | ||||
| @ -21,7 +21,7 @@ import MuteDropdownItem from './mute' | ||||
| import { DropdownItemUpVote } from './upvote' | ||||
| 
 | ||||
| export default function ItemInfo ({ | ||||
|   item, pendingSats, full, commentsText = 'comments', | ||||
|   item, full, commentsText = 'comments', | ||||
|   commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText, | ||||
|   onQuoteReply, nofollow, extraBadges | ||||
| }) { | ||||
| @ -40,8 +40,8 @@ export default function ItemInfo ({ | ||||
|   }, [item]) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (item) setMeTotalSats((item.meSats || 0) + (item.meAnonSats || 0) + (pendingSats || 0)) | ||||
|   }, [item?.meSats, item?.meAnonSats, pendingSats]) | ||||
|     if (item) setMeTotalSats((item.meSats || 0) + (item.meAnonSats || 0)) | ||||
|   }, [item?.meSats, item?.meAnonSats]) | ||||
| 
 | ||||
|   return ( | ||||
|     <div className={className || `${styles.other}`}> | ||||
| @ -57,7 +57,7 @@ export default function ItemInfo ({ | ||||
|               ? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}` | ||||
|               : ''} from me)`} `} | ||||
|           > | ||||
|             {numWithUnits(item.sats + pendingSats)} | ||||
|             {numWithUnits(item.sats)} | ||||
|           </span> | ||||
|           <span> \ </span> | ||||
|         </>} | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import Link from 'next/link' | ||||
| import styles from './item.module.css' | ||||
| import UpVote from './upvote' | ||||
| import { useRef, useState } from 'react' | ||||
| import { useRef } from 'react' | ||||
| import { AD_USER_ID, NOFOLLOW_LIMIT } from '../lib/constants' | ||||
| import Pin from '../svgs/pushpin-fill.svg' | ||||
| import reactStringReplace from 'react-string-replace' | ||||
| @ -27,7 +27,6 @@ export function SearchTitle ({ title }) { | ||||
| export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments, onQuoteReply }) { | ||||
|   const titleRef = useRef() | ||||
|   const router = useRouter() | ||||
|   const [pendingSats, setPendingSats] = useState(0) | ||||
| 
 | ||||
|   const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL) | ||||
|   const nofollow = item.sats + item.boost < NOFOLLOW_LIMIT && !item.position ? 'nofollow' : '' | ||||
| @ -47,7 +46,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s | ||||
|             ? <DownZap width={24} height={24} className={styles.dontLike} id={item.id} meDontLikeSats={item.meDontLikeSats} /> | ||||
|             : Number(item.user?.id) === AD_USER_ID | ||||
|               ? <AdIcon width={24} height={24} className={styles.ad} /> | ||||
|               : <UpVote item={item} className={styles.upvote} pendingSats={pendingSats} setPendingSats={setPendingSats} />} | ||||
|               : <UpVote item={item} className={styles.upvote} />} | ||||
|         <div className={styles.hunk}> | ||||
|           <div className={`${styles.main} flex-wrap`}> | ||||
|             <Link | ||||
| @ -93,7 +92,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s | ||||
|               </>} | ||||
|           </div> | ||||
|           <ItemInfo | ||||
|             full={full} item={item} pendingSats={pendingSats} | ||||
|             full={full} item={item} | ||||
|             onQuoteReply={onQuoteReply} | ||||
|             nofollow={nofollow} | ||||
|             extraBadges={Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>} | ||||
|  | ||||
| @ -54,7 +54,6 @@ export default function Items ({ ssrData, variables = {}, query, destructureData | ||||
| } | ||||
| 
 | ||||
| export function ListItem ({ item, ...props }) { | ||||
|   console.log(item) | ||||
|   return ( | ||||
|     item.parentId | ||||
|       ? <CommentFlat item={item} noReply includeParent clickToContext {...props} /> | ||||
|  | ||||
| @ -2,7 +2,7 @@ import UpBolt from '../svgs/bolt.svg' | ||||
| import styles from './upvote.module.css' | ||||
| import { gql, useMutation } from '@apollo/client' | ||||
| import ActionTooltip from './action-tooltip' | ||||
| import ItemAct, { useAct } from './item-act' | ||||
| import ItemAct, { useAct, useZap } from './item-act' | ||||
| import { useMe } from './me' | ||||
| import getColor from '../lib/rainbow' | ||||
| import { useCallback, useMemo, useRef, useState } from 'react' | ||||
| @ -10,11 +10,8 @@ import LongPressable from 'react-longpressable' | ||||
| import Overlay from 'react-bootstrap/Overlay' | ||||
| import Popover from 'react-bootstrap/Popover' | ||||
| import { useShowModal } from './modal' | ||||
| import { LightningConsumer, useLightning } from './lightning' | ||||
| import { useLightning } from './lightning' | ||||
| import { numWithUnits } from '../lib/format' | ||||
| import { payOrLoginError, useInvoiceModal } from './invoice' | ||||
| import useDebounceCallback from './use-debounce-callback' | ||||
| import { useToast } from './toast' | ||||
| import { Dropdown } from 'react-bootstrap' | ||||
| 
 | ||||
| const UpvotePopover = ({ target, show, handleClose }) => { | ||||
| @ -58,13 +55,12 @@ const TipPopover = ({ target, show, handleClose }) => ( | ||||
| 
 | ||||
| export function DropdownItemUpVote ({ item }) { | ||||
|   const showModal = useShowModal() | ||||
|   const strike = useLightning() | ||||
| 
 | ||||
|   return ( | ||||
|     <Dropdown.Item | ||||
|       onClick={async () => { | ||||
|         showModal(onClose => | ||||
|           <ItemAct onClose={onClose} itemId={item.id} strike={strike} />) | ||||
|           <ItemAct onClose={onClose} itemId={item.id} />) | ||||
|       }} | ||||
|     > | ||||
|       <span className='text-success'>zap</span> | ||||
| @ -72,14 +68,12 @@ export function DropdownItemUpVote ({ item }) { | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| export default function UpVote ({ item, className, pendingSats, setPendingSats }) { | ||||
| export default function UpVote ({ item, className }) { | ||||
|   const showModal = useShowModal() | ||||
|   const [voteShow, _setVoteShow] = useState(false) | ||||
|   const [tipShow, _setTipShow] = useState(false) | ||||
|   const ref = useRef() | ||||
|   const me = useMe() | ||||
|   const strike = useLightning() | ||||
|   const toaster = useToast() | ||||
|   const [setWalkthrough] = useMutation( | ||||
|     gql` | ||||
|       mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) { | ||||
| @ -116,64 +110,35 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats } | ||||
|   }, [me, tipShow, setWalkthrough]) | ||||
| 
 | ||||
|   const [act] = useAct() | ||||
| 
 | ||||
|   const showInvoiceModal = useInvoiceModal( | ||||
|     async ({ hash, hmac }, { variables }) => { | ||||
|       await act({ variables: { ...variables, hash, hmac } }) | ||||
|       strike() | ||||
|     }, [act, strike]) | ||||
| 
 | ||||
|   const zap = useDebounceCallback(async (sats) => { | ||||
|     if (!sats) return | ||||
|     const variables = { id: item.id, sats, act: 'TIP' } | ||||
| 
 | ||||
|     act({ | ||||
|       variables, | ||||
|       optimisticResponse: { | ||||
|         act: { | ||||
|           id: item.id, | ||||
|           sats, | ||||
|           path: item.path, | ||||
|           act: 'TIP' | ||||
|         } | ||||
|       } | ||||
|     }).catch((error) => { | ||||
|       if (payOrLoginError(error)) { | ||||
|         showInvoiceModal({ amount: sats }, { variables }) | ||||
|         return | ||||
|       } | ||||
|       console.error(error) | ||||
|       toaster.danger(error?.message || error?.toString?.()) | ||||
|     }) | ||||
|     setPendingSats(0) | ||||
|   }, 500, [act, toaster, item?.id, item?.path, showInvoiceModal, setPendingSats]) | ||||
|   const zap = useZap() | ||||
|   const strike = useLightning() | ||||
| 
 | ||||
|   const disabled = useMemo(() => item?.mine || item?.meForward || item?.deletedAt, | ||||
|     [item?.mine, item?.meForward, item?.deletedAt]) | ||||
| 
 | ||||
|   const [meSats, sats, overlayText, color] = useMemo(() => { | ||||
|     const meSats = (item?.meSats || item?.meAnonSats || 0) + pendingSats | ||||
|   const [meSats, overlayText, color] = useMemo(() => { | ||||
|     const meSats = (item?.meSats || item?.meAnonSats || 0) | ||||
| 
 | ||||
|     // what should our next tip be?
 | ||||
|     let sats = me?.privates?.tipDefault || 1 | ||||
|     let raiseSats = sats | ||||
|     if (me?.privates?.turboTipping) { | ||||
|       let raiseTip = sats | ||||
|       while (meSats >= raiseTip) { | ||||
|         raiseTip *= 10 | ||||
|       while (meSats >= raiseSats) { | ||||
|         raiseSats *= 10 | ||||
|       } | ||||
| 
 | ||||
|       sats = raiseTip - meSats | ||||
|       sats = raiseSats - meSats | ||||
|     } else { | ||||
|       raiseSats = meSats + sats | ||||
|     } | ||||
| 
 | ||||
|     return [meSats, sats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it', getColor(meSats)] | ||||
|   }, [item?.meSats, item?.meAnonSats, pendingSats, me?.privates?.tipDefault, me?.privates?.turboDefault]) | ||||
|     return [meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it', getColor(meSats)] | ||||
|   }, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault]) | ||||
| 
 | ||||
|   return ( | ||||
|     <LightningConsumer> | ||||
|       {(strike) => | ||||
|         <div ref={ref} className='upvoteParent'> | ||||
|           <LongPressable | ||||
|             onLongPress={ | ||||
|     <div ref={ref} className='upvoteParent'> | ||||
|       <LongPressable | ||||
|         onLongPress={ | ||||
|               async (e) => { | ||||
|                 if (!item) return | ||||
| 
 | ||||
| @ -184,10 +149,10 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats } | ||||
| 
 | ||||
|                 setTipShow(false) | ||||
|                 showModal(onClose => | ||||
|                   <ItemAct onClose={onClose} itemId={item.id} strike={strike} />) | ||||
|                   <ItemAct onClose={onClose} itemId={item.id} />) | ||||
|               } | ||||
|             } | ||||
|             onShortPress={ | ||||
|         onShortPress={ | ||||
|             me | ||||
|               ? async (e) => { | ||||
|                 if (!item) return | ||||
| @ -205,41 +170,36 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats } | ||||
| 
 | ||||
|                 strike() | ||||
| 
 | ||||
|                 setPendingSats(pendingSats => { | ||||
|                   const zapAmount = pendingSats + sats | ||||
|                   zap(zapAmount) | ||||
|                   return zapAmount | ||||
|                 }) | ||||
|                 zap({ item, me }) | ||||
|               } | ||||
|               : () => showModal(onClose => <ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />) | ||||
|               : () => showModal(onClose => <ItemAct onClose={onClose} itemId={item.id} act={act} />) | ||||
|           } | ||||
|       > | ||||
|         <ActionTooltip notForm disable={disabled} overlayText={overlayText}> | ||||
|           <div | ||||
|             className={`${disabled ? styles.noSelfTips : ''} ${styles.upvoteWrapper}`} | ||||
|           > | ||||
|             <ActionTooltip notForm disable={disabled} overlayText={overlayText}> | ||||
|               <div | ||||
|                 className={`${disabled ? styles.noSelfTips : ''} ${styles.upvoteWrapper}`} | ||||
|               > | ||||
|                 <UpBolt | ||||
|                   width={26} | ||||
|                   height={26} | ||||
|                   className={ | ||||
|             <UpBolt | ||||
|               width={26} | ||||
|               height={26} | ||||
|               className={ | ||||
|                       `${styles.upvote} | ||||
|                       ${className || ''} | ||||
|                       ${disabled ? styles.noSelfTips : ''} | ||||
|                       ${meSats ? styles.voted : ''}` | ||||
|                     } | ||||
|                   style={meSats | ||||
|                     ? { | ||||
|                         fill: color, | ||||
|                         filter: `drop-shadow(0 0 6px ${color}90)` | ||||
|                       } | ||||
|                     : undefined} | ||||
|                 /> | ||||
|               </div> | ||||
|             </ActionTooltip> | ||||
|           </LongPressable> | ||||
|           <TipPopover target={ref.current} show={tipShow} handleClose={() => setTipShow(false)} /> | ||||
|           <UpvotePopover target={ref.current} show={voteShow} handleClose={() => setVoteShow(false)} /> | ||||
|         </div>} | ||||
|     </LightningConsumer> | ||||
|               style={meSats | ||||
|                 ? { | ||||
|                     fill: color, | ||||
|                     filter: `drop-shadow(0 0 6px ${color}90)` | ||||
|                   } | ||||
|                 : undefined} | ||||
|             /> | ||||
|           </div> | ||||
|         </ActionTooltip> | ||||
|       </LongPressable> | ||||
|       <TipPopover target={ref.current} show={tipShow} handleClose={() => setTipShow(false)} /> | ||||
|       <UpvotePopover target={ref.current} show={voteShow} handleClose={() => setVoteShow(false)} /> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,88 @@ | ||||
| -- Update item_act to noop on negative or zero sats | ||||
| CREATE OR REPLACE FUNCTION item_act(item_id INTEGER, user_id INTEGER, act "ItemActType", act_sats INTEGER) | ||||
| RETURNS INTEGER | ||||
| LANGUAGE plpgsql | ||||
| AS $$ | ||||
| DECLARE | ||||
|     user_msats BIGINT; | ||||
|     act_msats BIGINT; | ||||
|     fee_msats BIGINT; | ||||
|     item_act_id INTEGER; | ||||
|     fwd_entry record; -- for loop iterator variable to iterate across forward recipients | ||||
|     fwd_msats BIGINT; -- for loop variable calculating how many msats to give each forward recipient | ||||
|     total_fwd_msats BIGINT := 0; -- accumulator to see how many msats have been forwarded for the act | ||||
| BEGIN | ||||
|     PERFORM ASSERT_SERIALIZED(); | ||||
| 
 | ||||
|     IF act_sats <= 0 THEN | ||||
|         RETURN 0; | ||||
|     END IF; | ||||
| 
 | ||||
|     act_msats := act_sats * 1000; | ||||
|     SELECT msats INTO user_msats FROM users WHERE id = user_id; | ||||
|     IF act_msats > user_msats THEN | ||||
|         RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS'; | ||||
|     END IF; | ||||
| 
 | ||||
|     -- deduct msats from actor | ||||
|     UPDATE users SET msats = msats - act_msats WHERE id = user_id; | ||||
| 
 | ||||
|     IF act = 'TIP' THEN | ||||
|         -- call to influence weightedVotes ... we need to do this before we record the acts because | ||||
|         -- the priors acts are taken into account | ||||
|         PERFORM weighted_votes_after_tip(item_id, user_id, act_sats); | ||||
|         -- call to denormalize sats and commentSats | ||||
|         PERFORM sats_after_tip(item_id, user_id, act_msats); | ||||
| 
 | ||||
|         -- take 10% and insert as FEE | ||||
|         fee_msats := CEIL(act_msats * 0.1); | ||||
|         act_msats := act_msats - fee_msats; | ||||
| 
 | ||||
|         -- save the fee act into item_act_id so we can record referral acts | ||||
|         INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) | ||||
|             VALUES (fee_msats, item_id, user_id, 'FEE', now_utc(), now_utc()) | ||||
|             RETURNING id INTO item_act_id; | ||||
| 
 | ||||
|         -- leave the rest as a tip | ||||
|         INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) | ||||
|             VALUES (act_msats, item_id, user_id, 'TIP', now_utc(), now_utc()); | ||||
| 
 | ||||
|         -- denormalize bounty paid (if applicable) | ||||
|         PERFORM bounty_paid_after_act(item_id, user_id); | ||||
| 
 | ||||
|         -- add sats to actees' balance and stacked count | ||||
|         FOR fwd_entry IN SELECT "userId", "pct" FROM "ItemForward" WHERE "itemId" = item_id | ||||
|         LOOP | ||||
|             -- fwd_msats represents the sats for this forward recipient from this particular tip action | ||||
|             fwd_msats := act_msats * fwd_entry.pct / 100; | ||||
|             -- keep track of how many msats have been forwarded, so we can give any remaining to OP | ||||
|             total_fwd_msats := fwd_msats + total_fwd_msats; | ||||
| 
 | ||||
|             UPDATE users | ||||
|             SET msats = msats + fwd_msats, "stackedMsats" = "stackedMsats" + fwd_msats | ||||
|             WHERE id = fwd_entry."userId"; | ||||
|         END LOOP; | ||||
| 
 | ||||
|         -- Give OP any remaining msats after forwards have been applied | ||||
|         IF act_msats - total_fwd_msats > 0 THEN | ||||
|             UPDATE users | ||||
|             SET msats = msats + act_msats - total_fwd_msats, "stackedMsats" = "stackedMsats" + act_msats - total_fwd_msats | ||||
|             WHERE id = (SELECT "userId" FROM "Item" WHERE id = item_id); | ||||
|         END IF; | ||||
|     ELSE -- BOOST, POLL, DONT_LIKE_THIS, STREAM | ||||
|         -- call to influence if DONT_LIKE_THIS weightedDownVotes | ||||
|         IF act = 'DONT_LIKE_THIS' THEN | ||||
|             PERFORM weighted_downvotes_after_act(item_id, user_id, act_sats); | ||||
|         END IF; | ||||
| 
 | ||||
|         INSERT INTO "ItemAct" (msats, "itemId", "userId", act, created_at, updated_at) | ||||
|             VALUES (act_msats, item_id, user_id, act, now_utc(), now_utc()) | ||||
|             RETURNING id INTO item_act_id; | ||||
|     END IF; | ||||
| 
 | ||||
|     -- store referral effects | ||||
|     PERFORM referral_act(item_act_id); | ||||
| 
 | ||||
|     RETURN 0; | ||||
| END; | ||||
| $$; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user