ca11ac9fb8
* wip backend optimism * another inch * make action state transitions only happen once * another inch * almost ready for testing * use interactive txs * another inch * ready for basic testing * lint fix * inches * wip item update * get item update to work * donate and downzap * inchy inch * fix territory paid actions * wip usePaidMutation * usePaidMutation error handling * PENDING_HELD and HELD transitions, gql paidAction return types * mostly working pessimism * make sure invoice field is present in optimisticResponse * inches * show optimistic values to current me * first pass at notifications and payment status reporting * fix migration to have withdrawal hash * reverse optimism on payment failure * Revert "Optimistic updates via pending sats in item context (#1229)" This reverts commit 93713b33df9bc3701dc5a692b86a04ff64e8cfb1. * add onCompleted to usePaidMutation * onPaid and onPayError for new comments * use 'IS DISTINCT FROM' for NULL invoiceActionState columns * make usePaidMutation easier to read * enhance invoice qr * prevent actions on unpaid items * allow navigation to action's invoice * retry create item * start edit window after item is paid for * fix ux of retries from notifications * refine retries * fix optimistic downzaps * remember item updates can't be retried * store reference to action item in invoice * remove invoice modal layout shift * fix destructuring * fix zap undos * make sure ItemAct is paid in aggregate queries * dont toast on long press zap undo * fix delete and remindme bots * optimistic poll votes with retries * fix retry notifications and invoice item context * fix pessimisitic typo * item mentions and mention notifications * dont show payment retry on item popover * make bios work * refactor paidAction transitions * remove stray console.log * restore docker compose nwc settings * add new todos * persist qr modal on post submission + unify item form submission * fix post edit threshold * make bounty payments work * make job posting work * remove more store procedure usage ... document serialization concerns * dont use dynamic imports for paid action modules * inline comment denormalization * create item starts with median votes * fix potential of serialization anomalies in zaps * dont trigger notification indicator on successful paid action invoices * ignore invoiceId on territory actions and add optimistic concurrency control * begin docs for paid actions * better error toasts and fix apollo cache warnings * small documentation enhancements * improve paid action docs * optimistic concurrency control for territory updates * use satsToMsats and msatsToSats helpers * explictly type raw query template parameters * improve consistency of nested relation names * complete paid action docs * useEffect for canEdit on payment * make sure invoiceId is provided when required * don't return null when expecting array * remove buy credits * move verifyPayment to paidAction * fix comments invoicePaidAt time zone * close nwc connections once * grouped logs for paid actions * stop invoiceWaitUntilPaid if not attempting to pay * allow actionState to transition directly from HELD to PAID * make paid mutation wait until pessimistic are fully paid * change button text when form submits/pays * pulsing form submit button * ignore me in notification indicator for territory subscription * filter unpaid items from more queries * fix donation stike timing * fix pending poll vote * fix recent item notifcation padding * no default form submitting button text * don't show paying on submit button on free edits * fix territory autorenew with fee credits * reorg readme * allow jobs to be editted forever * fix image uploads * more filter fixes for aggregate views * finalize paid action invoice expirations * remove unnecessary async * keep clientside cache normal/consistent * add more detail to paid action doc * improve paid action table * remove actionType guard * fix top territories * typo api/paidAction/README.md Co-authored-by: ekzyis <ek@stacker.news> * typo components/use-paid-mutation.js Co-authored-by: ekzyis <ek@stacker.news> * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> * encorporate ek feeback * more ek suggestions * fix 'cost to post' hover on items * Apply suggestions from code review Co-authored-by: ekzyis <ek@stacker.news> --------- Co-authored-by: ekzyis <ek@stacker.news>
242 lines
7.3 KiB
JavaScript
242 lines
7.3 KiB
JavaScript
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, { ZapUndoController, useZap } from './item-act'
|
|
import { useMe } from './me'
|
|
import getColor from '@/lib/rainbow'
|
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
import LongPressable from './long-pressable'
|
|
import Overlay from 'react-bootstrap/Overlay'
|
|
import Popover from 'react-bootstrap/Popover'
|
|
import { useShowModal } from './modal'
|
|
import { numWithUnits } from '@/lib/format'
|
|
import { Dropdown } from 'react-bootstrap'
|
|
|
|
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?.privates?.tipDefault || 1} more sat{me?.privates?.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 function DropdownItemUpVote ({ item }) {
|
|
const showModal = useShowModal()
|
|
|
|
return (
|
|
<Dropdown.Item
|
|
onClick={async () => {
|
|
showModal(onClose =>
|
|
<ItemAct onClose={onClose} item={item} />)
|
|
}}
|
|
>
|
|
<span className='text-success'>zap</span>
|
|
</Dropdown.Item>
|
|
)
|
|
}
|
|
|
|
export const nextTip = (meSats, { tipDefault, turboTipping }) => {
|
|
// what should our next tip be?
|
|
if (!turboTipping) return (tipDefault || 1)
|
|
|
|
let sats = tipDefault || 1
|
|
if (turboTipping) {
|
|
while (meSats >= sats) {
|
|
sats *= 10
|
|
}
|
|
// deduct current sats since turbo tipping is about total zap not making the next zap 10x
|
|
sats -= meSats
|
|
}
|
|
|
|
return sats
|
|
}
|
|
|
|
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 [hover, setHover] = useState(false)
|
|
const [setWalkthrough] = useMutation(
|
|
gql`
|
|
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
|
|
setWalkthrough(upvotePopover: $upvotePopover, tipPopover: $tipPopover)
|
|
}`
|
|
)
|
|
|
|
const [controller, setController] = useState(null)
|
|
const [pending, setPending] = useState(false)
|
|
|
|
const setVoteShow = useCallback((yes) => {
|
|
if (!me) return
|
|
|
|
// if they haven't seen the walkthrough and they have sats
|
|
if (yes && !me.privates?.upvotePopover && me.privates?.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.privates?.tipPopover && me.privates?.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 zap = useZap()
|
|
|
|
const disabled = useMemo(() => item?.mine || item?.meForward || item?.deletedAt,
|
|
[item?.mine, item?.meForward, item?.deletedAt])
|
|
|
|
const [meSats, overlayText, color, nextColor] = useMemo(() => {
|
|
const meSats = (item?.meSats || item?.meAnonSats || 0)
|
|
|
|
// what should our next tip be?
|
|
const sats = nextTip(meSats, { ...me?.privates })
|
|
|
|
return [
|
|
meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it',
|
|
getColor(meSats), getColor(meSats + sats)]
|
|
}, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault])
|
|
|
|
const handleModalClosed = () => {
|
|
setHover(false)
|
|
}
|
|
|
|
const handleLongPress = (e) => {
|
|
if (!item) return
|
|
|
|
// we can't tip ourselves
|
|
if (disabled) {
|
|
return
|
|
}
|
|
|
|
setTipShow(false)
|
|
|
|
if (pending) {
|
|
controller.abort()
|
|
setController(null)
|
|
return
|
|
}
|
|
const c = new ZapUndoController({ onStart: () => setPending(true), onDone: () => setPending(false) })
|
|
setController(c)
|
|
|
|
showModal(onClose =>
|
|
<ItemAct onClose={onClose} item={item} abortSignal={c.signal} />, { onClose: handleModalClosed })
|
|
}
|
|
|
|
const handleShortPress = async () => {
|
|
if (me) {
|
|
if (!item) return
|
|
|
|
// we can't tip ourselves
|
|
if (disabled) {
|
|
return
|
|
}
|
|
|
|
if (meSats) {
|
|
setVoteShow(false)
|
|
} else {
|
|
setTipShow(true)
|
|
}
|
|
|
|
if (pending) {
|
|
controller.abort()
|
|
setController(null)
|
|
return
|
|
}
|
|
const c = new ZapUndoController({ onStart: () => setPending(true), onDone: () => setPending(false) })
|
|
setController(c)
|
|
|
|
await zap({ item, me, abortSignal: c.signal })
|
|
} else {
|
|
showModal(onClose => <ItemAct onClose={onClose} item={item} />, { onClose: handleModalClosed })
|
|
}
|
|
}
|
|
|
|
const fillColor = hover || pending ? nextColor : color
|
|
|
|
return (
|
|
<div ref={ref} className='upvoteParent'>
|
|
<LongPressable
|
|
onLongPress={handleLongPress}
|
|
onShortPress={handleShortPress}
|
|
>
|
|
<ActionTooltip notForm disable={disabled} overlayText={overlayText}>
|
|
<div
|
|
className={`${disabled ? styles.noSelfTips : ''} ${styles.upvoteWrapper}`}
|
|
>
|
|
<UpBolt
|
|
onPointerEnter={() => setHover(true)}
|
|
onMouseLeave={() => setHover(false)}
|
|
onTouchEnd={() => setHover(false)}
|
|
width={26}
|
|
height={26}
|
|
className={
|
|
`${styles.upvote}
|
|
${className || ''}
|
|
${disabled ? styles.noSelfTips : ''}
|
|
${meSats ? styles.voted : ''}
|
|
${pending ? styles.pending : ''}`
|
|
}
|
|
style={meSats || hover || pending
|
|
? {
|
|
fill: fillColor,
|
|
filter: `drop-shadow(0 0 6px ${fillColor}90)`
|
|
}
|
|
: undefined}
|
|
/>
|
|
</div>
|
|
</ActionTooltip>
|
|
</LongPressable>
|
|
<TipPopover target={ref.current} show={tipShow} handleClose={() => setTipShow(false)} />
|
|
<UpvotePopover target={ref.current} show={voteShow} handleClose={() => setVoteShow(false)} />
|
|
</div>
|
|
)
|
|
}
|