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 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' import { InvoiceModal, payOrLoginError } from './invoice' 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 ( <InvoiceModal 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> ) }