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' import classNames from 'classnames' 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?.tipRandom ? 'a random amount of' : `${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 defaultTipIncludingRandom = ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax } = {}) => { return tipRandom ? Math.floor((Math.random() * (tipRandomMax - tipRandomMin + 1)) + tipRandomMin) : (tipDefault || 100) } export const nextTip = (meSats, { tipDefault, turboTipping, tipRandom, tipRandomMin, tipRandomMax }) => { if (turboTipping) { if (tipRandom) { let pow = 0 // find the first power of 10 that is greater than meSats while (!(meSats <= tipRandomMax * 10 ** pow)) { pow++ } // if meSats is in that power of 10's range already, move into the next range if (meSats >= tipRandomMin * 10 ** pow) { pow++ } // make sure the our range minimum doesn't overlap with the previous range maximum tipRandomMin = tipRandomMax * 10 ** (pow - 1) >= tipRandomMin * 10 ** pow ? tipRandomMax * 10 ** (pow - 1) + 1 : tipRandomMin * 10 ** pow tipRandomMax = tipRandomMax * 10 ** pow return Math.floor((Math.random() * (tipRandomMax - tipRandomMin + 1)) + tipRandomMin) - meSats } let sats = defaultTipIncludingRandom({ tipDefault, tipRandom, tipRandomMin, tipRandomMax }) while (meSats >= sats) { sats *= 10 } // deduct current sats since turbo tipping is about total zap not making the next zap 10x return sats - meSats } return defaultTipIncludingRandom({ tipDefault, tipRandom, tipRandomMin, tipRandomMax }) } export default function UpVote ({ item, className, collapsed }) { const showModal = useShowModal() const [voteShow, _setVoteShow] = useState(false) const [tipShow, _setTipShow] = useState(false) const ref = useRef() const { me } = useMe() const [setWalkthrough] = useMutation( gql` mutation setWalkthrough($upvotePopover: Boolean, $tipPopover: Boolean) { setWalkthrough(upvotePopover: $upvotePopover, tipPopover: $tipPopover) }` ) const [controller, setController] = useState(null) const [pending, setPending] = useState(0) 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(() => collapsed || item?.mine || item?.meForward || item?.deletedAt, [collapsed, item?.mine, item?.meForward, item?.deletedAt]) const [meSats, overlayText, color, nextColor] = useMemo(() => { const meSats = (me ? item?.meSats : item?.meAnonSats) || 0 // what should our next tip be? const sats = pending || nextTip(meSats, { ...me?.privates }) let overlayTextContent if (me) { overlayTextContent = me.privates?.tipRandom ? 'random' : numWithUnits(sats, { abbreviate: false }) } else { overlayTextContent = 'zap it' } return [ meSats, overlayTextContent, getColor(meSats), getColor(meSats + sats)] }, [ me, item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault, me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax, pending]) 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: (sats) => setPending(sats), onDone: () => setPending(0) }) setController(c) showModal(onClose => <ItemAct onClose={onClose} item={item} abortSignal={c.signal} />) } 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: (sats) => setPending(sats), onDone: () => setPending(0) }) setController(c) await zap({ item, me, abortSignal: c.signal }) } else { showModal(onClose => <ItemAct onClose={onClose} item={item} />) } } const style = useMemo(() => ({ '--hover-fill': nextColor, '--hover-filter': `drop-shadow(0 0 6px ${nextColor}90)`, '--fill': color, '--filter': `drop-shadow(0 0 6px ${color}90)` }), [color, nextColor]) return ( <div ref={ref} className='upvoteParent'> <LongPressable onLongPress={handleLongPress} onShortPress={handleShortPress} > <ActionTooltip notForm disable={disabled} overlayText={overlayText}> <div className={classNames(disabled && styles.noSelfTips, styles.upvoteWrapper)}> <UpBolt width={26} height={26} className={classNames(styles.upvote, className, disabled && styles.noSelfTips, meSats && styles.voted, pending && styles.pending)} style={style} /> </div> </ActionTooltip> </LongPressable> <TipPopover target={ref.current} show={tipShow} handleClose={() => setTipShow(false)} /> <UpvotePopover target={ref.current} show={voteShow} handleClose={() => setVoteShow(false)} /> </div> ) }