ekzyis ac45fdc234
Use HODL invoices (#432)
* Use HODL invoices

* Fix expiry check comparing string with Date

* Fix unconfirmed user balance for HODL invoices

This is done by syncing the data from LND to the Invoice table.

If the columns is_held and msatsReceived are set, the frontend is told that we're ready to execute the action.

We then update the user balance in the same tx as the action.

We need to still keep checking the invoice for expiration though.

* Fix worker acting upon deleted invoices

* Prevent usage of invoice after expiration

* Use onComplete from <Countdown> to show expired status

* Remove unused lnd argument

* Fix item destructuring from query

* Fix balance added to every stacker

* Fix hmac required

* Fix invoices not used when logged in

* refactor: move invoiceable code into form

* renamed invoiceHash, invoiceHmac to hash, hmac since it's less verbose all over the place
* form now supports `invoiceable` in its props
* form then wraps `onSubmit` with `useInvoiceable` and passes optional invoice options

* Show expired if expired and canceled

* Also use useCallback for zapping

* Always expire modal invoices after 3m

* little styling thing

---------

Co-authored-by: ekzyis <ek@stacker.news>
Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
2023-08-30 21:48:49 -05:00

296 lines
9.1 KiB
JavaScript

import UpBolt from '../svgs/bolt.svg'
import styles from './upvote.module.css'
import { gql, useMutation } from '@apollo/client'
import FundError, { payOrLoginError } from './fund-error'
import ActionTooltip from './action-tooltip'
import ItemAct from './item-act'
import { useMe } from './me'
import Rainbow from '../lib/rainbow'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
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 { numWithUnits } from '../lib/format'
const getColor = (meSats) => {
if (!meSats || meSats <= 10) {
return 'var(--bs-secondary)'
}
const idx = Math.min(
Math.floor((Math.log(meSats) / Math.log(10000)) * (Rainbow.length - 1)),
Rainbow.length - 1)
return Rainbow[idx]
}
const UpvotePopover = ({ target, show, handleClose }) => {
const me = useMe()
return (
<Overlay
show={show}
target={target}
placement='right'
>
<Popover id='popover-basic'>
<Popover.Header className='d-flex justify-content-between alert-dismissible' as='h4'>Zapping
<button type='button' className='btn-close' onClick={handleClose}><span className='visually-hidden-focusable'>Close alert</span></button>
</Popover.Header>
<Popover.Body>
<div className='mb-2'>Press the bolt again to zap {me?.tipDefault || 1} more sat{me?.tipDefault > 1 ? 's' : ''}.</div>
<div>Repeatedly press the bolt to zap more sats.</div>
</Popover.Body>
</Popover>
</Overlay>
)
}
const TipPopover = ({ target, show, handleClose }) => (
<Overlay
show={show}
target={target}
placement='right'
>
<Popover id='popover-basic'>
<Popover.Header className='d-flex justify-content-between alert-dismissible' as='h4'>Press and hold
<button type='button' className='btn-close' onClick={handleClose}><span className='visually-hidden-focusable'>Close alert</span></button>
</Popover.Header>
<Popover.Body>
<div className='mb-2'>Press and hold bolt to zap a custom amount.</div>
<div>As you zap more, the bolt color follows the rainbow.</div>
</Popover.Body>
</Popover>
</Overlay>
)
export default function UpVote ({ item, className, pendingSats, setPendingSats }) {
const showModal = useShowModal()
const [voteShow, _setVoteShow] = useState(false)
const [tipShow, _setTipShow] = useState(false)
const ref = useRef()
const timerRef = useRef(null)
const me = useMe()
const strike = useLightning()
const [setWalkthrough] = useMutation(
gql`
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
setWalkthrough(upvotePopover: $upvotePopover, tipPopover: $tipPopover)
}`
)
const setVoteShow = useCallback((yes) => {
if (!me) return
// if they haven't seen the walkthrough and they have sats
if (yes && !me.upvotePopover && me.sats) {
_setVoteShow(true)
}
if (voteShow && !yes) {
_setVoteShow(false)
setWalkthrough({ variables: { upvotePopover: true } })
}
}, [me, voteShow, setWalkthrough])
const setTipShow = useCallback((yes) => {
if (!me) return
// if we want to show it, yet we still haven't shown
if (yes && !me.tipPopover && me.sats) {
_setTipShow(true)
}
// if it's currently showing and we want to hide it
if (tipShow && !yes) {
_setTipShow(false)
setWalkthrough({ variables: { tipPopover: true } })
}
}, [me, tipShow, setWalkthrough])
const [act] = useMutation(
gql`
mutation act($id: ID!, $sats: Int!, $hash: String, $hmac: String) {
act(id: $id, sats: $sats, hash: $hash, hmac: $hmac) {
sats
}
}`, {
update (cache, { data: { act: { sats } } }) {
cache.modify({
id: `Item:${item.id}`,
fields: {
sats (existingSats = 0) {
return existingSats + sats
},
meSats: me
? (existingSats = 0) => {
if (sats <= me.sats) {
if (existingSats === 0) {
setVoteShow(true)
} else {
setTipShow(true)
}
}
return existingSats + sats
}
: undefined
}
})
// update all ancestors
item.path.split('.').forEach(id => {
if (Number(id) === Number(item.id)) return
cache.modify({
id: `Item:${id}`,
fields: {
commentSats (existingCommentSats = 0) {
return existingCommentSats + sats
}
}
})
})
}
}
)
// if we want to use optimistic response, we need to buffer the votes
// because if someone votes in quick succession, responses come back out of order
// so we wait a bit to see if there are more votes coming in
// this effectively performs our own debounced optimistic response
useEffect(() => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
if (pendingSats > 0) {
timerRef.current = setTimeout(async (sats) => {
const variables = { id: item.id, sats: pendingSats }
try {
timerRef.current && setPendingSats(0)
await act({
variables,
optimisticResponse: {
act: {
sats
}
}
})
} catch (error) {
if (payOrLoginError(error)) {
showModal(onClose => {
return (
<FundError
onClose={onClose}
amount={pendingSats}
onPayment={async ({ hash, hmac }) => {
await act({ variables: { ...variables, hash, hmac } })
strike()
}}
/>
)
})
return
}
if (!timerRef.current) return
throw new Error({ message: error.toString() })
}
}, 500, pendingSats)
}
return async () => {
clearTimeout(timerRef.current)
timerRef.current = null
}
}, [pendingSats, act, item, showModal, setPendingSats])
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
// what should our next tip be?
let sats = me?.tipDefault || 1
if (me?.turboTipping) {
let raiseTip = sats
while (meSats >= raiseTip) {
raiseTip *= 10
}
sats = raiseTip - meSats
}
return [meSats, sats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it', getColor(meSats)]
}, [item?.meSats, item?.meAnonSats, pendingSats, me?.tipDefault, me?.turboDefault])
return (
<LightningConsumer>
{(strike) =>
<div ref={ref} className='upvoteParent'>
<LongPressable
onLongPress={
async (e) => {
if (!item) return
// we can't tip ourselves
if (disabled) {
return
}
setTipShow(false)
showModal(onClose =>
<ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
}
}
onShortPress={
me
? async (e) => {
if (!item) return
// we can't tip ourselves
if (disabled) {
return
}
if (meSats) {
setVoteShow(false)
}
strike()
setPendingSats(pendingSats + sats)
}
: () => showModal(onClose => <ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
}
>
<ActionTooltip notForm disable={disabled} overlayText={overlayText}>
<div
className={`${disabled ? styles.noSelfTips : ''} ${styles.upvoteWrapper}`}
>
<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>
)
}