idempotent zaps
This commit is contained in:
parent
374a7985da
commit
73ad93f2bb
|
@ -786,6 +786,52 @@ export default {
|
||||||
|
|
||||||
notifyZapped({ models, id })
|
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 {
|
return {
|
||||||
id,
|
id,
|
||||||
sats,
|
sats,
|
||||||
|
|
|
@ -37,6 +37,7 @@ export default gql`
|
||||||
updateNoteId(id: ID!, noteId: String!): Item!
|
updateNoteId(id: ID!, noteId: String!): Item!
|
||||||
upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: 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!
|
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!
|
pollVote(id: ID!, hash: String, hmac: String): ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,6 @@ export default function Comment ({
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const [pendingSats, setPendingSats] = useState(0)
|
|
||||||
const { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text })
|
const { ref: textRef, quote, quoteReply, cancelQuote } = useQuoteReply({ text: item.text })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -148,7 +147,7 @@ export default function Comment ({
|
||||||
<div className={`${itemStyles.item} ${styles.item}`}>
|
<div className={`${itemStyles.item} ${styles.item}`}>
|
||||||
{item.meDontLikeSats > item.meSats
|
{item.meDontLikeSats > item.meSats
|
||||||
? <DownZap width={24} height={24} className={styles.dontLike} id={item.id} meDontLikeSats={item.meDontLikeSats} />
|
? <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={`${itemStyles.hunk} ${styles.hunk}`}>
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
{item.user?.meMute && !includeParent && collapse === 'yep'
|
{item.user?.meMute && !includeParent && collapse === 'yep'
|
||||||
|
@ -162,7 +161,6 @@ export default function Comment ({
|
||||||
</span>)
|
</span>)
|
||||||
: <ItemInfo
|
: <ItemInfo
|
||||||
item={item}
|
item={item}
|
||||||
pendingSats={pendingSats}
|
|
||||||
commentsText='replies'
|
commentsText='replies'
|
||||||
commentTextSingular='reply'
|
commentTextSingular='reply'
|
||||||
className={`${itemStyles.other} ${styles.other}`}
|
className={`${itemStyles.other} ${styles.other}`}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import AccordianItem from './accordian-item'
|
||||||
import Flag from '../svgs/flag-fill.svg'
|
import Flag from '../svgs/flag-fill.svg'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import getColor from '../lib/rainbow'
|
import getColor from '../lib/rainbow'
|
||||||
import { useLightning } from './lightning'
|
|
||||||
|
|
||||||
export function DownZap ({ id, meDontLikeSats, ...props }) {
|
export function DownZap ({ id, meDontLikeSats, ...props }) {
|
||||||
const style = useMemo(() => (meDontLikeSats
|
const style = useMemo(() => (meDontLikeSats
|
||||||
|
@ -23,7 +22,6 @@ export function DownZap ({ id, meDontLikeSats, ...props }) {
|
||||||
function DownZapper ({ id, As, children }) {
|
function DownZapper ({ id, As, children }) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const strike = useLightning()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<As
|
<As
|
||||||
|
@ -34,7 +32,7 @@ function DownZapper ({ id, As, children }) {
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
onClose()
|
onClose()
|
||||||
toaster.success('item downzapped')
|
toaster.success('item downzapped')
|
||||||
}} itemId={id} strike={strike} down
|
}} itemId={id} down
|
||||||
>
|
>
|
||||||
<AccordianItem
|
<AccordianItem
|
||||||
header='what is a downzap?' body={
|
header='what is a downzap?' body={
|
||||||
|
|
|
@ -6,6 +6,9 @@ import { useMe } from './me'
|
||||||
import UpBolt from '../svgs/bolt.svg'
|
import UpBolt from '../svgs/bolt.svg'
|
||||||
import { amountSchema } from '../lib/validate'
|
import { amountSchema } from '../lib/validate'
|
||||||
import { gql, useMutation } from '@apollo/client'
|
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]
|
const defaultTips = [100, 1000, 10000, 100000]
|
||||||
|
|
||||||
|
@ -37,10 +40,11 @@ const addCustomTip = (amount) => {
|
||||||
window.localStorage.setItem('custom-tips', JSON.stringify(customTips))
|
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 inputRef = useRef(null)
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const [oValue, setOValue] = useState()
|
const [oValue, setOValue] = useState()
|
||||||
|
const strike = useLightning()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus()
|
inputRef.current?.focus()
|
||||||
|
@ -63,7 +67,7 @@ export default function ItemAct ({ onClose, itemId, down, strike, children }) {
|
||||||
hmac
|
hmac
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
strike && await strike()
|
await strike()
|
||||||
addCustomTip(Number(amount))
|
addCustomTip(Number(amount))
|
||||||
onClose()
|
onClose()
|
||||||
}, [act, down, itemId, strike])
|
}, [act, down, itemId, strike])
|
||||||
|
@ -166,3 +170,110 @@ export function useAct ({ onUpdate } = {}) {
|
||||||
}`, { update }
|
}`, { 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'
|
import { DropdownItemUpVote } from './upvote'
|
||||||
|
|
||||||
export default function ItemInfo ({
|
export default function ItemInfo ({
|
||||||
item, pendingSats, full, commentsText = 'comments',
|
item, full, commentsText = 'comments',
|
||||||
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
||||||
onQuoteReply, nofollow, extraBadges
|
onQuoteReply, nofollow, extraBadges
|
||||||
}) {
|
}) {
|
||||||
|
@ -40,8 +40,8 @@ export default function ItemInfo ({
|
||||||
}, [item])
|
}, [item])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (item) setMeTotalSats((item.meSats || 0) + (item.meAnonSats || 0) + (pendingSats || 0))
|
if (item) setMeTotalSats((item.meSats || 0) + (item.meAnonSats || 0))
|
||||||
}, [item?.meSats, item?.meAnonSats, pendingSats])
|
}, [item?.meSats, item?.meAnonSats])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className || `${styles.other}`}>
|
<div className={className || `${styles.other}`}>
|
||||||
|
@ -57,7 +57,7 @@ export default function ItemInfo ({
|
||||||
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
? ` & ${numWithUnits(item.meDontLikeSats, { abbreviate: false, unitSingular: 'downsat', unitPlural: 'downsats' })}`
|
||||||
: ''} from me)`} `}
|
: ''} from me)`} `}
|
||||||
>
|
>
|
||||||
{numWithUnits(item.sats + pendingSats)}
|
{numWithUnits(item.sats)}
|
||||||
</span>
|
</span>
|
||||||
<span> \ </span>
|
<span> \ </span>
|
||||||
</>}
|
</>}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import styles from './item.module.css'
|
import styles from './item.module.css'
|
||||||
import UpVote from './upvote'
|
import UpVote from './upvote'
|
||||||
import { useRef, useState } from 'react'
|
import { useRef } from 'react'
|
||||||
import { AD_USER_ID, NOFOLLOW_LIMIT } from '../lib/constants'
|
import { AD_USER_ID, NOFOLLOW_LIMIT } from '../lib/constants'
|
||||||
import Pin from '../svgs/pushpin-fill.svg'
|
import Pin from '../svgs/pushpin-fill.svg'
|
||||||
import reactStringReplace from 'react-string-replace'
|
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 }) {
|
export default function Item ({ item, rank, belowTitle, right, full, children, siblingComments, onQuoteReply }) {
|
||||||
const titleRef = useRef()
|
const titleRef = useRef()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [pendingSats, setPendingSats] = useState(0)
|
|
||||||
|
|
||||||
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
|
const image = item.url && item.url.startsWith(process.env.NEXT_PUBLIC_IMGPROXY_URL)
|
||||||
const nofollow = item.sats + item.boost < NOFOLLOW_LIMIT && !item.position ? 'nofollow' : ''
|
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} />
|
? <DownZap width={24} height={24} className={styles.dontLike} id={item.id} meDontLikeSats={item.meDontLikeSats} />
|
||||||
: Number(item.user?.id) === AD_USER_ID
|
: Number(item.user?.id) === AD_USER_ID
|
||||||
? <AdIcon width={24} height={24} className={styles.ad} />
|
? <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.hunk}>
|
||||||
<div className={`${styles.main} flex-wrap`}>
|
<div className={`${styles.main} flex-wrap`}>
|
||||||
<Link
|
<Link
|
||||||
|
@ -93,7 +92,7 @@ export default function Item ({ item, rank, belowTitle, right, full, children, s
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
<ItemInfo
|
<ItemInfo
|
||||||
full={full} item={item} pendingSats={pendingSats}
|
full={full} item={item}
|
||||||
onQuoteReply={onQuoteReply}
|
onQuoteReply={onQuoteReply}
|
||||||
nofollow={nofollow}
|
nofollow={nofollow}
|
||||||
extraBadges={Number(item?.user?.id) === AD_USER_ID && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
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 }) {
|
export function ListItem ({ item, ...props }) {
|
||||||
console.log(item)
|
|
||||||
return (
|
return (
|
||||||
item.parentId
|
item.parentId
|
||||||
? <CommentFlat item={item} noReply includeParent clickToContext {...props} />
|
? <CommentFlat item={item} noReply includeParent clickToContext {...props} />
|
||||||
|
|
|
@ -2,7 +2,7 @@ import UpBolt from '../svgs/bolt.svg'
|
||||||
import styles from './upvote.module.css'
|
import styles from './upvote.module.css'
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { gql, useMutation } from '@apollo/client'
|
||||||
import ActionTooltip from './action-tooltip'
|
import ActionTooltip from './action-tooltip'
|
||||||
import ItemAct, { useAct } from './item-act'
|
import ItemAct, { useAct, useZap } from './item-act'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import getColor from '../lib/rainbow'
|
import getColor from '../lib/rainbow'
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||||
|
@ -10,11 +10,8 @@ import LongPressable from 'react-longpressable'
|
||||||
import Overlay from 'react-bootstrap/Overlay'
|
import Overlay from 'react-bootstrap/Overlay'
|
||||||
import Popover from 'react-bootstrap/Popover'
|
import Popover from 'react-bootstrap/Popover'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import { LightningConsumer, useLightning } from './lightning'
|
import { useLightning } from './lightning'
|
||||||
import { numWithUnits } from '../lib/format'
|
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'
|
import { Dropdown } from 'react-bootstrap'
|
||||||
|
|
||||||
const UpvotePopover = ({ target, show, handleClose }) => {
|
const UpvotePopover = ({ target, show, handleClose }) => {
|
||||||
|
@ -58,13 +55,12 @@ const TipPopover = ({ target, show, handleClose }) => (
|
||||||
|
|
||||||
export function DropdownItemUpVote ({ item }) {
|
export function DropdownItemUpVote ({ item }) {
|
||||||
const showModal = useShowModal()
|
const showModal = useShowModal()
|
||||||
const strike = useLightning()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
showModal(onClose =>
|
showModal(onClose =>
|
||||||
<ItemAct onClose={onClose} itemId={item.id} strike={strike} />)
|
<ItemAct onClose={onClose} itemId={item.id} />)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className='text-success'>zap</span>
|
<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 showModal = useShowModal()
|
||||||
const [voteShow, _setVoteShow] = useState(false)
|
const [voteShow, _setVoteShow] = useState(false)
|
||||||
const [tipShow, _setTipShow] = useState(false)
|
const [tipShow, _setTipShow] = useState(false)
|
||||||
const ref = useRef()
|
const ref = useRef()
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const strike = useLightning()
|
|
||||||
const toaster = useToast()
|
|
||||||
const [setWalkthrough] = useMutation(
|
const [setWalkthrough] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
|
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
|
||||||
|
@ -116,64 +110,35 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
||||||
}, [me, tipShow, setWalkthrough])
|
}, [me, tipShow, setWalkthrough])
|
||||||
|
|
||||||
const [act] = useAct()
|
const [act] = useAct()
|
||||||
|
const zap = useZap()
|
||||||
const showInvoiceModal = useInvoiceModal(
|
const strike = useLightning()
|
||||||
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 disabled = useMemo(() => item?.mine || item?.meForward || item?.deletedAt,
|
const disabled = useMemo(() => item?.mine || item?.meForward || item?.deletedAt,
|
||||||
[item?.mine, item?.meForward, item?.deletedAt])
|
[item?.mine, item?.meForward, item?.deletedAt])
|
||||||
|
|
||||||
const [meSats, sats, overlayText, color] = useMemo(() => {
|
const [meSats, overlayText, color] = useMemo(() => {
|
||||||
const meSats = (item?.meSats || item?.meAnonSats || 0) + pendingSats
|
const meSats = (item?.meSats || item?.meAnonSats || 0)
|
||||||
|
|
||||||
// what should our next tip be?
|
// what should our next tip be?
|
||||||
let sats = me?.privates?.tipDefault || 1
|
let sats = me?.privates?.tipDefault || 1
|
||||||
|
let raiseSats = sats
|
||||||
if (me?.privates?.turboTipping) {
|
if (me?.privates?.turboTipping) {
|
||||||
let raiseTip = sats
|
while (meSats >= raiseSats) {
|
||||||
while (meSats >= raiseTip) {
|
raiseSats *= 10
|
||||||
raiseTip *= 10
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sats = raiseTip - meSats
|
sats = raiseSats - meSats
|
||||||
|
} else {
|
||||||
|
raiseSats = meSats + sats
|
||||||
}
|
}
|
||||||
|
|
||||||
return [meSats, sats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it', getColor(meSats)]
|
return [meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it', getColor(meSats)]
|
||||||
}, [item?.meSats, item?.meAnonSats, pendingSats, me?.privates?.tipDefault, me?.privates?.turboDefault])
|
}, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LightningConsumer>
|
<div ref={ref} className='upvoteParent'>
|
||||||
{(strike) =>
|
<LongPressable
|
||||||
<div ref={ref} className='upvoteParent'>
|
onLongPress={
|
||||||
<LongPressable
|
|
||||||
onLongPress={
|
|
||||||
async (e) => {
|
async (e) => {
|
||||||
if (!item) return
|
if (!item) return
|
||||||
|
|
||||||
|
@ -184,10 +149,10 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
||||||
|
|
||||||
setTipShow(false)
|
setTipShow(false)
|
||||||
showModal(onClose =>
|
showModal(onClose =>
|
||||||
<ItemAct onClose={onClose} itemId={item.id} strike={strike} />)
|
<ItemAct onClose={onClose} itemId={item.id} />)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onShortPress={
|
onShortPress={
|
||||||
me
|
me
|
||||||
? async (e) => {
|
? async (e) => {
|
||||||
if (!item) return
|
if (!item) return
|
||||||
|
@ -205,41 +170,36 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
||||||
|
|
||||||
strike()
|
strike()
|
||||||
|
|
||||||
setPendingSats(pendingSats => {
|
zap({ item, me })
|
||||||
const zapAmount = pendingSats + sats
|
|
||||||
zap(zapAmount)
|
|
||||||
return zapAmount
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
: () => 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}>
|
<UpBolt
|
||||||
<div
|
width={26}
|
||||||
className={`${disabled ? styles.noSelfTips : ''} ${styles.upvoteWrapper}`}
|
height={26}
|
||||||
>
|
className={
|
||||||
<UpBolt
|
|
||||||
width={26}
|
|
||||||
height={26}
|
|
||||||
className={
|
|
||||||
`${styles.upvote}
|
`${styles.upvote}
|
||||||
${className || ''}
|
${className || ''}
|
||||||
${disabled ? styles.noSelfTips : ''}
|
${disabled ? styles.noSelfTips : ''}
|
||||||
${meSats ? styles.voted : ''}`
|
${meSats ? styles.voted : ''}`
|
||||||
}
|
}
|
||||||
style={meSats
|
style={meSats
|
||||||
? {
|
? {
|
||||||
fill: color,
|
fill: color,
|
||||||
filter: `drop-shadow(0 0 6px ${color}90)`
|
filter: `drop-shadow(0 0 6px ${color}90)`
|
||||||
}
|
}
|
||||||
: undefined}
|
: undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ActionTooltip>
|
</ActionTooltip>
|
||||||
</LongPressable>
|
</LongPressable>
|
||||||
<TipPopover target={ref.current} show={tipShow} handleClose={() => setTipShow(false)} />
|
<TipPopover target={ref.current} show={tipShow} handleClose={() => setTipShow(false)} />
|
||||||
<UpvotePopover target={ref.current} show={voteShow} handleClose={() => setVoteShow(false)} />
|
<UpvotePopover target={ref.current} show={voteShow} handleClose={() => setVoteShow(false)} />
|
||||||
</div>}
|
</div>
|
||||||
</LightningConsumer>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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…
Reference in New Issue