import { useState, useEffect, useMemo, useCallback } from 'react' import AccordianItem from './accordian-item' import { Input, InputUserSuggest, VariableInput, Checkbox } from './form' import InputGroup from 'react-bootstrap/InputGroup' import { BOOST_MIN, BOOST_MULT, MAX_FORWARDS, SSR } from '@/lib/constants' import { DEFAULT_CROSSPOSTING_RELAYS } from '@/lib/nostr' import Info from './info' import { abbrNum, numWithUnits } from '@/lib/format' import styles from './adv-post-form.module.css' import { useMe } from './me' import { useFeeButton } from './fee-button' import { useRouter } from 'next/router' import { useFormikContext } from 'formik' import { gql, useQuery } from '@apollo/client' import useDebounceCallback from './use-debounce-callback' import { Button } from 'react-bootstrap' import classNames from 'classnames' const EMPTY_FORWARD = { nym: '', pct: '' } export function AdvPostInitial ({ forward, boost }) { return { boost: boost || '', forward: forward?.length ? forward : [EMPTY_FORWARD] } } const FormStatus = { DIRTY: 'dirty', ERROR: 'error' } export function BoostHelp () { return (
  1. Boost ranks items higher based on the amount
  2. The highest boost in a territory over the last 30 days is pinned to the top of the territory
  3. The highest boost across all territories over the last 30 days is pinned to the top of the homepage
  4. The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}
  5. Each {numWithUnits(BOOST_MULT, { abbreviate: false })} of boost is equivalent to a zap-vote from a maximally trusted stacker
  6. The decay of boost "votes" increases at 1.25x the rate of organic votes
  7. boost can take a few minutes to show higher ranking in feed
  8. 100% of boost goes to the territory founder and top stackers as rewards
) } export function BoostInput ({ onChange, ...props }) { const feeButton = useFeeButton() let merge if (feeButton) { ({ merge } = feeButton) } return ( boost } name='boost' onChange={(_, e) => { merge?.({ boost: { term: `+ ${e.target.value}`, label: 'boost', op: '+', modifier: cost => cost + Number(e.target.value) } }) onChange && onChange(_, e) }} hint={ranks posts higher temporarily based on the amount} append={sats} {...props} /> ) } const BoostMaxes = ({ subName, homeMax, subMax, boost, updateBoost }) => { return (
{subName && }
) } // act means we are adding to existing boost export function BoostItemInput ({ item, sub, act = false, ...props }) { // act adds boost to existing boost const existingBoost = act ? Number(item?.boost || 0) : 0 const [boost, setBoost] = useState(act ? 0 : Number(item?.boost || 0)) const { data, previousData, refetch } = useQuery(gql` query BoostPosition($sub: String, $id: ID, $boost: Int) { boostPosition(sub: $sub, id: $id, boost: $boost) { home sub homeMaxBoost subMaxBoost } }`, { variables: { sub: item?.subName || sub?.name, boost: existingBoost + boost, id: item?.id }, fetchPolicy: 'cache-and-network', skip: !!item?.parentId || SSR }) const getPositionDebounce = useDebounceCallback((...args) => refetch(...args), 1000, [refetch]) const updateBoost = useCallback((boost) => { const boostToUse = Number(boost || 0) setBoost(boostToUse) getPositionDebounce({ sub: item?.subName || sub?.name, boost: Number(existingBoost + boostToUse), id: item?.id }) }, [getPositionDebounce, item?.id, item?.subName, sub?.name, existingBoost]) const dat = data || previousData const boostMessage = useMemo(() => { if (!item?.parentId && boost >= BOOST_MULT) { if (dat?.boostPosition?.home || dat?.boostPosition?.sub || boost > dat?.boostPosition?.homeMaxBoost || boost > dat?.boostPosition?.subMaxBoost) { const boostPinning = [] if (dat?.boostPosition?.home || boost > dat?.boostPosition?.homeMaxBoost) { boostPinning.push('homepage') } if ((item?.subName || sub?.name) && (dat?.boostPosition?.sub || boost > dat?.boostPosition?.subMaxBoost)) { boostPinning.push(`~${item?.subName || sub?.name}`) } return `pins to the top of ${boostPinning.join(' and ')}` } } return 'ranks posts higher based on the amount' }, [boost, dat?.boostPosition?.home, dat?.boostPosition?.sub, item?.subName, sub?.name]) return ( <> {boostMessage}} onChange={(_, e) => { if (e.target.value >= 0) { updateBoost(Number(e.target.value)) } }} overrideValue={boost} {...props} groupClassName='mb-1' /> {!item?.parentId && } ) } export default function AdvPostForm ({ children, item, sub, storageKeyPrefix }) { const { me } = useMe() const router = useRouter() const [itemType, setItemType] = useState() const formik = useFormikContext() const [show, setShow] = useState(false) useEffect(() => { const isDirty = formik?.values.forward?.[0].nym !== '' || formik?.values.forward?.[0].pct !== '' || formik?.values.boost !== '' || (router.query?.type === 'link' && formik?.values.text !== '') // if the adv post form is dirty on first render, show the accordian if (isDirty) { setShow(FormStatus.DIRTY) } // HACK ... TODO: we should generically handle this kind of local storage stuff // in the form component, overriding the initial values if (storageKeyPrefix) { for (let i = 0; i < MAX_FORWARDS; i++) { ['nym', 'pct'].forEach(key => { const value = window.localStorage.getItem(`${storageKeyPrefix}-forward[${i}].${key}`) if (value) { formik?.setFieldValue(`forward[${i}].${key}`, value) } }) } } }, [formik?.values, storageKeyPrefix]) useEffect(() => { // force show the accordian if there is an error and the form is submitting const hasError = !!formik?.errors?.boost || formik?.errors?.forward?.length > 0 // if it's open we don't want to collapse on submit setShow(show => hasError && formik?.isSubmitting ? FormStatus.ERROR : show) }, [formik?.isSubmitting]) useEffect(() => { const determineItemType = () => { if (router && router.query.type) { return router.query.type } else if (item) { const typeMap = { url: 'link', bounty: 'bounty', pollCost: 'poll' } for (const [key, type] of Object.entries(typeMap)) { if (item[key]) { return type } } return 'discussion' } } const type = determineItemType() setItemType(type) }, [item, router]) function renderCrosspostDetails (itemType) { switch (itemType) { case 'discussion': return
  • crosspost this discussion as a NIP-23 event
  • case 'link': return
  • crosspost this link as a NIP-01 event
  • case 'bounty': return
  • crosspost this bounty as a NIP-99 event
  • case 'poll': return
  • crosspost this poll as a NIP-41 event
  • default: return null } } return ( options} show={show} body={ <> {children} Forward sats to up to 5 other stackers. Any remaining sats go to you.} > {({ index, placeholder }) => { return (
    @} showValid groupClassName={`${styles.name} me-3 mb-0`} /> %} groupClassName={`${styles.percent} mb-0`} />
    ) }}
    {me && itemType && crosspost to nostr
      {renderCrosspostDetails(itemType)}
    • requires NIP-07 extension for signing
    • we use your NIP-05 relays if set
    • we use these relays by default:
      • {DEFAULT_CROSSPOSTING_RELAYS.map((relay, i) => (
      • {relay}
      • ))}
    } name='crosspost' />} } /> ) }