2021-12-05 17:37:55 +00:00
|
|
|
|
import UpBolt from '../svgs/bolt.svg'
|
2021-04-22 22:14:32 +00:00
|
|
|
|
import styles from './upvote.module.css'
|
2021-04-26 21:55:15 +00:00
|
|
|
|
import { gql, useMutation } from '@apollo/client'
|
2023-01-10 23:13:37 +00:00
|
|
|
|
import FundError from './fund-error'
|
2021-07-08 18:42:57 +00:00
|
|
|
|
import ActionTooltip from './action-tooltip'
|
2023-01-10 23:13:37 +00:00
|
|
|
|
import ItemAct from './item-act'
|
2021-09-12 16:55:38 +00:00
|
|
|
|
import { useMe } from './me'
|
2021-12-13 19:49:34 +00:00
|
|
|
|
import Rainbow from '../lib/rainbow'
|
2023-07-23 15:08:43 +00:00
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
2021-10-30 16:52:24 +00:00
|
|
|
|
import LongPressable from 'react-longpressable'
|
2023-07-24 18:35:05 +00:00
|
|
|
|
import Overlay from 'react-bootstrap/Overlay'
|
|
|
|
|
import Popover from 'react-bootstrap/Popover'
|
2023-01-10 23:13:37 +00:00
|
|
|
|
import { useShowModal } from './modal'
|
2023-07-05 14:47:44 +00:00
|
|
|
|
import { LightningConsumer } from './lightning'
|
2021-12-09 20:40:40 +00:00
|
|
|
|
|
|
|
|
|
const getColor = (meSats) => {
|
|
|
|
|
if (!meSats || meSats <= 10) {
|
2023-07-24 18:35:05 +00:00
|
|
|
|
return 'var(--bs-secondary)'
|
2021-12-09 20:40:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const idx = Math.min(
|
2021-12-13 19:49:34 +00:00
|
|
|
|
Math.floor((Math.log(meSats) / Math.log(10000)) * (Rainbow.length - 1)),
|
2021-12-09 20:40:40 +00:00
|
|
|
|
Rainbow.length - 1)
|
|
|
|
|
return Rainbow[idx]
|
|
|
|
|
}
|
|
|
|
|
|
2022-04-12 21:09:12 +00:00
|
|
|
|
const UpvotePopover = ({ target, show, handleClose }) => {
|
|
|
|
|
const me = useMe()
|
|
|
|
|
return (
|
|
|
|
|
<Overlay
|
|
|
|
|
show={show}
|
|
|
|
|
target={target}
|
|
|
|
|
placement='right'
|
|
|
|
|
>
|
|
|
|
|
<Popover id='popover-basic'>
|
2023-07-24 18:35:05 +00:00
|
|
|
|
<Popover.Body className='d-flex justify-content-between alert-dismissible' as='h3'>Zapping
|
2023-07-29 19:38:20 +00:00
|
|
|
|
<button type='button' className='close' onClick={handleClose}><span aria-hidden='true'>×</span><span className='visually-hidden-focusable'>Close alert</span></button>
|
2023-07-24 18:35:05 +00:00
|
|
|
|
</Popover.Body>
|
|
|
|
|
<Popover.Body>
|
2023-06-19 18:21:55 +00:00
|
|
|
|
<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>
|
2023-07-24 18:35:05 +00:00
|
|
|
|
</Popover.Body>
|
2022-04-12 21:09:12 +00:00
|
|
|
|
</Popover>
|
|
|
|
|
</Overlay>
|
|
|
|
|
)
|
|
|
|
|
}
|
2021-12-09 20:40:40 +00:00
|
|
|
|
|
|
|
|
|
const TipPopover = ({ target, show, handleClose }) => (
|
|
|
|
|
<Overlay
|
|
|
|
|
show={show}
|
|
|
|
|
target={target}
|
|
|
|
|
placement='right'
|
|
|
|
|
>
|
|
|
|
|
<Popover id='popover-basic'>
|
2023-07-24 18:35:05 +00:00
|
|
|
|
<Popover.Body className='d-flex justify-content-between alert-dismissible' as='h3'>Press and hold
|
2023-07-29 19:38:20 +00:00
|
|
|
|
<button type='button' className='close' onClick={handleClose}><span aria-hidden='true'>×</span><span className='visually-hidden-focusable'>Close alert</span></button>
|
2023-07-24 18:35:05 +00:00
|
|
|
|
</Popover.Body>
|
|
|
|
|
<Popover.Body>
|
2023-06-19 18:21:55 +00:00
|
|
|
|
<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>
|
2023-07-24 18:35:05 +00:00
|
|
|
|
</Popover.Body>
|
2021-12-09 20:40:40 +00:00
|
|
|
|
</Popover>
|
|
|
|
|
</Overlay>
|
|
|
|
|
)
|
2021-04-26 21:55:15 +00:00
|
|
|
|
|
2023-07-09 16:15:46 +00:00
|
|
|
|
export default function UpVote ({ item, className, pendingSats, setPendingSats }) {
|
2023-01-10 23:13:37 +00:00
|
|
|
|
const showModal = useShowModal()
|
2021-12-09 20:40:40 +00:00
|
|
|
|
const [voteShow, _setVoteShow] = useState(false)
|
|
|
|
|
const [tipShow, _setTipShow] = useState(false)
|
|
|
|
|
const ref = useRef()
|
2023-07-09 16:15:46 +00:00
|
|
|
|
const timerRef = useRef(null)
|
2021-09-12 16:55:38 +00:00
|
|
|
|
const me = useMe()
|
2021-12-09 20:40:40 +00:00
|
|
|
|
const [setWalkthrough] = useMutation(
|
|
|
|
|
gql`
|
|
|
|
|
mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) {
|
|
|
|
|
setWalkthrough(upvotePopover: $upvotePopover, tipPopover: $tipPopover)
|
|
|
|
|
}`
|
|
|
|
|
)
|
|
|
|
|
|
2023-07-23 15:08:43 +00:00
|
|
|
|
const setVoteShow = useCallback((yes) => {
|
2021-12-09 20:40:40 +00:00
|
|
|
|
if (!me) return
|
|
|
|
|
|
2022-03-10 20:09:09 +00:00
|
|
|
|
// if they haven't seen the walkthrough and they have sats
|
|
|
|
|
if (yes && !me.upvotePopover && me.sats) {
|
|
|
|
|
_setVoteShow(true)
|
2021-12-09 20:40:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (voteShow && !yes) {
|
2022-03-10 20:09:09 +00:00
|
|
|
|
_setVoteShow(false)
|
2021-12-09 20:40:40 +00:00
|
|
|
|
setWalkthrough({ variables: { upvotePopover: true } })
|
|
|
|
|
}
|
2023-07-23 15:08:43 +00:00
|
|
|
|
}, [me, voteShow, setWalkthrough])
|
2021-12-09 20:40:40 +00:00
|
|
|
|
|
2023-07-23 15:08:43 +00:00
|
|
|
|
const setTipShow = useCallback((yes) => {
|
2021-12-09 20:40:40 +00:00
|
|
|
|
if (!me) return
|
|
|
|
|
|
|
|
|
|
// if we want to show it, yet we still haven't shown
|
2022-03-10 20:09:09 +00:00
|
|
|
|
if (yes && !me.tipPopover && me.sats) {
|
|
|
|
|
_setTipShow(true)
|
2021-12-09 20:40:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if it's currently showing and we want to hide it
|
|
|
|
|
if (tipShow && !yes) {
|
2022-03-10 20:09:09 +00:00
|
|
|
|
_setTipShow(false)
|
2021-12-09 20:40:40 +00:00
|
|
|
|
setWalkthrough({ variables: { tipPopover: true } })
|
|
|
|
|
}
|
2023-07-23 15:08:43 +00:00
|
|
|
|
}, [me, tipShow, setWalkthrough])
|
2021-12-09 20:40:40 +00:00
|
|
|
|
|
2021-09-08 21:51:23 +00:00
|
|
|
|
const [act] = useMutation(
|
2021-04-26 21:55:15 +00:00
|
|
|
|
gql`
|
2023-07-20 14:55:28 +00:00
|
|
|
|
mutation act($id: ID!, $sats: Int!, $invoiceHash: String) {
|
|
|
|
|
act(id: $id, sats: $sats, invoiceHash: $invoiceHash) {
|
2021-09-10 21:13:52 +00:00
|
|
|
|
sats
|
|
|
|
|
}
|
2021-04-26 21:55:15 +00:00
|
|
|
|
}`, {
|
2023-07-09 16:15:46 +00:00
|
|
|
|
update (cache, { data: { act: { sats } } }) {
|
2021-04-26 21:55:15 +00:00
|
|
|
|
cache.modify({
|
2021-09-10 21:13:52 +00:00
|
|
|
|
id: `Item:${item.id}`,
|
2021-04-26 21:55:15 +00:00
|
|
|
|
fields: {
|
|
|
|
|
sats (existingSats = 0) {
|
2022-01-20 23:04:12 +00:00
|
|
|
|
return existingSats + sats
|
2021-04-27 21:30:58 +00:00
|
|
|
|
},
|
2023-07-13 03:08:32 +00:00
|
|
|
|
meSats: me
|
|
|
|
|
? (existingSats = 0) => {
|
|
|
|
|
if (sats <= me.sats) {
|
|
|
|
|
if (existingSats === 0) {
|
|
|
|
|
setVoteShow(true)
|
|
|
|
|
} else {
|
|
|
|
|
setTipShow(true)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-10 23:13:37 +00:00
|
|
|
|
|
2023-07-13 03:08:32 +00:00
|
|
|
|
return existingSats + sats
|
|
|
|
|
}
|
|
|
|
|
: undefined
|
2021-04-26 21:55:15 +00:00
|
|
|
|
}
|
|
|
|
|
})
|
2022-09-01 21:53:39 +00:00
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
2021-04-26 21:55:15 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
2021-04-22 22:14:32 +00:00
|
|
|
|
|
2023-07-09 16:15:46 +00:00
|
|
|
|
// 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
|
2023-07-11 18:33:13 +00:00
|
|
|
|
useEffect(() => {
|
2023-07-09 16:15:46 +00:00
|
|
|
|
if (timerRef.current) {
|
|
|
|
|
clearTimeout(timerRef.current)
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-11 18:33:13 +00:00
|
|
|
|
if (pendingSats > 0) {
|
2023-07-23 15:08:43 +00:00
|
|
|
|
timerRef.current = setTimeout(async (sats) => {
|
2023-07-20 22:34:39 +00:00
|
|
|
|
const variables = { id: item.id, sats: pendingSats }
|
2023-07-11 18:33:13 +00:00
|
|
|
|
try {
|
|
|
|
|
timerRef.current && setPendingSats(0)
|
|
|
|
|
await act({
|
2023-07-20 22:34:39 +00:00
|
|
|
|
variables,
|
2023-07-11 18:33:13 +00:00
|
|
|
|
optimisticResponse: {
|
|
|
|
|
act: {
|
2023-07-23 15:08:43 +00:00
|
|
|
|
sats
|
2023-07-11 18:33:13 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-07-09 16:15:46 +00:00
|
|
|
|
})
|
2023-07-11 18:33:13 +00:00
|
|
|
|
} catch (error) {
|
2023-07-23 15:08:43 +00:00
|
|
|
|
if (!timerRef.current) return
|
|
|
|
|
|
2023-07-11 18:33:13 +00:00
|
|
|
|
if (error.toString().includes('insufficient funds')) {
|
|
|
|
|
showModal(onClose => {
|
2023-07-20 22:34:39 +00:00
|
|
|
|
return (
|
|
|
|
|
<FundError
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
amount={pendingSats}
|
|
|
|
|
onPayment={async (_, invoiceHash) => {
|
|
|
|
|
await act({ variables: { ...variables, invoiceHash } })
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)
|
2023-07-11 18:33:13 +00:00
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
throw new Error({ message: error.toString() })
|
2023-07-09 16:15:46 +00:00
|
|
|
|
}
|
2023-07-23 15:08:43 +00:00
|
|
|
|
}, 500, pendingSats)
|
2023-07-11 18:33:13 +00:00
|
|
|
|
}
|
2023-07-09 16:15:46 +00:00
|
|
|
|
|
2023-07-23 15:08:43 +00:00
|
|
|
|
return async () => {
|
2023-07-11 18:33:13 +00:00
|
|
|
|
clearTimeout(timerRef.current)
|
|
|
|
|
timerRef.current = null
|
|
|
|
|
}
|
2023-07-23 15:08:43 +00:00
|
|
|
|
}, [pendingSats, act, item, showModal, setPendingSats])
|
2023-07-09 16:15:46 +00:00
|
|
|
|
|
2023-07-11 18:33:13 +00:00
|
|
|
|
const disabled = useMemo(() => {
|
|
|
|
|
return item?.mine || (me && Number(me.id) === item?.fwdUserId) || item?.deletedAt
|
|
|
|
|
}, [me?.id, item?.fwdUserId, item?.mine, item?.deletedAt])
|
2023-07-09 16:15:46 +00:00
|
|
|
|
|
2023-07-13 03:08:32 +00:00
|
|
|
|
const [meSats, meTotalSats, sats, overlayText, color] = useMemo(() => {
|
2023-07-11 18:33:13 +00:00
|
|
|
|
const meSats = (item?.meSats || 0) + pendingSats
|
2023-07-13 03:08:32 +00:00
|
|
|
|
const meTotalSats = meSats + (item?.meAnonSats || 0)
|
2022-12-09 19:25:38 +00:00
|
|
|
|
|
2023-07-11 18:33:13 +00:00
|
|
|
|
// what should our next tip be?
|
|
|
|
|
let sats = me?.tipDefault || 1
|
|
|
|
|
if (me?.turboTipping && me) {
|
|
|
|
|
let raiseTip = sats
|
|
|
|
|
while (meSats >= raiseTip) {
|
|
|
|
|
raiseTip *= 10
|
|
|
|
|
}
|
2022-12-09 19:25:38 +00:00
|
|
|
|
|
2023-07-11 18:33:13 +00:00
|
|
|
|
sats = raiseTip - meSats
|
|
|
|
|
}
|
2021-09-12 16:55:38 +00:00
|
|
|
|
|
2023-07-13 03:08:32 +00:00
|
|
|
|
return [meSats, meTotalSats, sats, `${sats} sat${sats > 1 ? 's' : ''}`, getColor(meTotalSats)]
|
|
|
|
|
}, [item?.meSats, item?.meAnonSats, pendingSats, me?.tipDefault, me?.turboDefault])
|
2023-01-12 23:53:09 +00:00
|
|
|
|
|
2021-04-22 22:14:32 +00:00
|
|
|
|
return (
|
2023-07-05 14:47:44 +00:00
|
|
|
|
<LightningConsumer>
|
2023-07-23 15:08:43 +00:00
|
|
|
|
{(strike) =>
|
2022-01-20 19:03:48 +00:00
|
|
|
|
<div ref={ref} className='upvoteParent'>
|
2021-12-09 20:40:40 +00:00
|
|
|
|
<LongPressable
|
|
|
|
|
onLongPress={
|
2021-10-30 16:52:24 +00:00
|
|
|
|
async (e) => {
|
2022-01-20 23:23:21 +00:00
|
|
|
|
if (!item) return
|
2021-10-30 16:52:24 +00:00
|
|
|
|
|
|
|
|
|
// we can't tip ourselves
|
2023-01-12 23:53:09 +00:00
|
|
|
|
if (disabled) {
|
2021-10-30 16:52:24 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-09 20:40:40 +00:00
|
|
|
|
setTipShow(false)
|
2023-01-10 23:13:37 +00:00
|
|
|
|
showModal(onClose =>
|
|
|
|
|
<ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
|
2021-10-30 16:52:24 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-12-09 20:40:40 +00:00
|
|
|
|
onShortPress={
|
2021-09-24 23:04:59 +00:00
|
|
|
|
me
|
2021-06-24 23:56:01 +00:00
|
|
|
|
? async (e) => {
|
2023-07-25 14:14:45 +00:00
|
|
|
|
if (!item) return
|
2021-09-12 16:55:38 +00:00
|
|
|
|
|
2023-07-25 14:14:45 +00:00
|
|
|
|
// we can't tip ourselves
|
|
|
|
|
if (disabled) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2021-09-12 16:55:38 +00:00
|
|
|
|
|
2023-07-25 14:14:45 +00:00
|
|
|
|
if (meSats) {
|
|
|
|
|
setVoteShow(false)
|
|
|
|
|
}
|
2021-09-10 18:55:36 +00:00
|
|
|
|
|
2023-07-25 14:14:45 +00:00
|
|
|
|
strike()
|
2021-09-10 18:55:36 +00:00
|
|
|
|
|
2023-07-25 14:14:45 +00:00
|
|
|
|
setPendingSats(pendingSats + sats)
|
|
|
|
|
}
|
2023-07-13 03:08:32 +00:00
|
|
|
|
: () => showModal(onClose => <ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
|
2021-05-20 19:41:21 +00:00
|
|
|
|
}
|
2021-12-09 20:40:40 +00:00
|
|
|
|
>
|
2023-07-11 18:33:13 +00:00
|
|
|
|
<ActionTooltip notForm disable={disabled} overlayText={overlayText}>
|
2021-12-09 20:40:40 +00:00
|
|
|
|
<div
|
2023-07-11 18:33:13 +00:00
|
|
|
|
className={`${disabled ? styles.noSelfTips : ''} ${styles.upvoteWrapper}`}
|
2021-12-09 20:40:40 +00:00
|
|
|
|
>
|
2021-12-06 15:32:33 +00:00
|
|
|
|
<UpBolt
|
2023-07-25 18:32:48 +00:00
|
|
|
|
width={26}
|
|
|
|
|
height={26}
|
2021-12-06 15:32:33 +00:00
|
|
|
|
className={
|
2021-12-09 20:40:40 +00:00
|
|
|
|
`${styles.upvote}
|
|
|
|
|
${className || ''}
|
2023-01-12 23:53:09 +00:00
|
|
|
|
${disabled ? styles.noSelfTips : ''}
|
2023-07-13 03:08:32 +00:00
|
|
|
|
${meTotalSats ? styles.voted : ''}`
|
2021-12-09 20:40:40 +00:00
|
|
|
|
}
|
2023-07-13 03:08:32 +00:00
|
|
|
|
style={meTotalSats
|
2021-12-06 15:32:33 +00:00
|
|
|
|
? {
|
|
|
|
|
fill: color,
|
|
|
|
|
filter: `drop-shadow(0 0 6px ${color}90)`
|
|
|
|
|
}
|
|
|
|
|
: undefined}
|
|
|
|
|
/>
|
2021-12-09 20:40:40 +00:00
|
|
|
|
</div>
|
|
|
|
|
</ActionTooltip>
|
|
|
|
|
</LongPressable>
|
|
|
|
|
<TipPopover target={ref.current} show={tipShow} handleClose={() => setTipShow(false)} />
|
|
|
|
|
<UpvotePopover target={ref.current} show={voteShow} handleClose={() => setVoteShow(false)} />
|
|
|
|
|
</div>}
|
2023-07-05 14:47:44 +00:00
|
|
|
|
</LightningConsumer>
|
2021-04-22 22:14:32 +00:00
|
|
|
|
)
|
|
|
|
|
}
|