make fee button less of a hack
This commit is contained in:
parent
3499f92436
commit
c23e49872a
|
@ -8,6 +8,7 @@ import { numWithUnits } from '../lib/format'
|
|||
import styles from './adv-post-form.module.css'
|
||||
import { useMe } from './me'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useFeeButton } from './fee-button'
|
||||
|
||||
const EMPTY_FORWARD = { nym: '', pct: '' }
|
||||
|
||||
|
@ -21,6 +22,7 @@ export function AdvPostInitial ({ forward, boost }) {
|
|||
export default function AdvPostForm ({ children }) {
|
||||
const me = useMe()
|
||||
const router = useRouter()
|
||||
const { merge } = useFeeButton()
|
||||
|
||||
return (
|
||||
<AccordianItem
|
||||
|
@ -51,6 +53,13 @@ export default function AdvPostForm ({ children }) {
|
|||
</div>
|
||||
}
|
||||
name='boost'
|
||||
onChange={(_, e) => merge({
|
||||
boost: {
|
||||
term: `+ ${e.target.value}`,
|
||||
label: 'boost',
|
||||
modifier: cost => cost + Number(e.target.value)
|
||||
}
|
||||
})}
|
||||
hint={<span className='text-muted'>ranks posts higher temporarily based on the amount</span>}
|
||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||
/>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import { Form, Input, MarkdownInput } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
||||
import FeeButton, { EditFeeButton } from './fee-button'
|
||||
import FeeButton from './fee-button'
|
||||
import InputGroup from 'react-bootstrap/InputGroup'
|
||||
import { bountySchema } from '../lib/validate'
|
||||
import { SubSelectInitial } from './sub-select-form'
|
||||
|
@ -133,29 +133,12 @@ export function BountyForm ({
|
|||
}
|
||||
/>
|
||||
<AdvPostForm edit={!!item} />
|
||||
<div className='mt-3'>
|
||||
{item
|
||||
? (
|
||||
<div className='d-flex'>
|
||||
<div className='d-flex mt-3'>
|
||||
<CancelButton />
|
||||
<EditFeeButton
|
||||
paidSats={item.meSats}
|
||||
parentId={null}
|
||||
text='save'
|
||||
ChildButton={SubmitButton}
|
||||
variant='secondary'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<FeeButton
|
||||
baseFee={1}
|
||||
parentId={null}
|
||||
text={buttonText}
|
||||
ChildButton={SubmitButton}
|
||||
variant='secondary'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
|
|
|
@ -4,6 +4,6 @@ import Button from 'react-bootstrap/Button'
|
|||
export default function CancelButton ({ onClick }) {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<Button className='me-3 text-muted nav-link fw-bold' variant='link' onClick={onClick || (() => router.back())}>cancel</Button>
|
||||
<Button className='me-4 text-muted nav-link fw-bold' variant='link' onClick={onClick || (() => router.back())}>cancel</Button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Form, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import { Form, MarkdownInput } from '../components/form'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import styles from './reply.module.css'
|
||||
import { EditFeeButton } from './fee-button'
|
||||
import Button from 'react-bootstrap/Button'
|
||||
import Delete from './delete'
|
||||
import { commentSchema } from '../lib/validate'
|
||||
import FeeButton, { FeeButtonProvider } from './fee-button'
|
||||
|
||||
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
|
||||
const [upsertComment] = useMutation(
|
||||
|
@ -29,6 +29,7 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
|
|||
|
||||
return (
|
||||
<div className={`${styles.reply} mt-2`}>
|
||||
<FeeButtonProvider>
|
||||
<Form
|
||||
initial={{
|
||||
text: comment.text
|
||||
|
@ -54,12 +55,15 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
|
|||
<Delete itemId={comment.id} onDelete={onSuccess} type='comment'>
|
||||
<Button variant='grey-medium'>delete</Button>
|
||||
</Delete>
|
||||
<EditFeeButton
|
||||
paidSats={comment.meSats}
|
||||
parentId={comment.parentId} text='save' ChildButton={SubmitButton} variant='secondary'
|
||||
<div className='d-flex mt-3'>
|
||||
<FeeButton
|
||||
text='save'
|
||||
variant='secondary'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</FeeButtonProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import { Form, Input, MarkdownInput } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
||||
import FeeButton, { EditFeeButton } from './fee-button'
|
||||
import FeeButton from './fee-button'
|
||||
import { ITEM_FIELDS } from '../fragments/items'
|
||||
import AccordianItem from './accordian-item'
|
||||
import Item from './item'
|
||||
|
@ -134,24 +134,19 @@ export function DiscussionForm ({
|
|||
/>
|
||||
<AdvPostForm edit={!!item} />
|
||||
<div className='mt-3'>
|
||||
{item
|
||||
? (
|
||||
<div className='d-flex justify-content-between'>
|
||||
{item &&
|
||||
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
|
||||
<Button variant='grey-medium'>delete</Button>
|
||||
</Delete>
|
||||
<div className='d-flex'>
|
||||
</Delete>}
|
||||
<div className='d-flex align-items-center ms-auto'>
|
||||
<CancelButton />
|
||||
<EditFeeButton
|
||||
paidSats={item.meSats}
|
||||
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
|
||||
<FeeButton
|
||||
text={buttonText}
|
||||
variant='secondary'
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
: <FeeButton
|
||||
baseFee={1} parentId={null} text={buttonText}
|
||||
ChildButton={SubmitButton} variant='secondary'
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
{!item &&
|
||||
<div className={`mt-3 ${related.length > 0 ? '' : 'invisible'}`}>
|
||||
|
|
|
@ -1,44 +1,131 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useEffect, useContext, createContext, useState, useCallback, useMemo } from 'react'
|
||||
import Table from 'react-bootstrap/Table'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import Info from './info'
|
||||
import styles from './fee-button.module.css'
|
||||
import { gql, useQuery } from '@apollo/client'
|
||||
import { useFormikContext } from 'formik'
|
||||
import { SSR, ANON_COMMENT_FEE, ANON_POST_FEE } from '../lib/constants'
|
||||
import { SSR } from '../lib/constants'
|
||||
import { numWithUnits } from '../lib/format'
|
||||
import { useMe } from './me'
|
||||
import AnonIcon from '../svgs/spy-fill.svg'
|
||||
import { useShowModal } from './modal'
|
||||
import Link from 'next/link'
|
||||
import { SubmitButton } from './form'
|
||||
|
||||
function Receipt ({ cost, repetition, imageFeesInfo, baseFee, parentId, boost }) {
|
||||
const FeeButtonContext = createContext()
|
||||
|
||||
export function postCommentBaseLineItems ({ baseCost = 1, comment = false, me }) {
|
||||
// XXX this doesn't match the logic on the server but it has the same
|
||||
// result on fees ... will need to change the server logic to match
|
||||
const anonCharge = me
|
||||
? {}
|
||||
: {
|
||||
anonCharge: {
|
||||
term: 'x 100',
|
||||
label: 'anon mult',
|
||||
modifier: (cost) => cost * 100
|
||||
}
|
||||
}
|
||||
return {
|
||||
baseCost: {
|
||||
term: baseCost,
|
||||
label: `${comment ? 'comment' : 'post'} cost`,
|
||||
modifier: (cost) => cost + baseCost
|
||||
},
|
||||
...anonCharge
|
||||
}
|
||||
}
|
||||
|
||||
export function postCommentUseRemoteLineItems ({ parentId, me } = {}) {
|
||||
if (!me) return () => {}
|
||||
const query = parentId
|
||||
? gql`{ itemRepetition(parentId: "${parentId}") }`
|
||||
: gql`{ itemRepetition }`
|
||||
return function useRemoteLineItems () {
|
||||
const [line, setLine] = useState({})
|
||||
|
||||
const { data } = useQuery(query, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||
|
||||
useEffect(() => {
|
||||
const repetition = data?.itemRepetition || 0
|
||||
if (!repetition) return
|
||||
setLine({
|
||||
itemRepetition: {
|
||||
term: <>x 10<sup>{repetition}</sup></>,
|
||||
label: <>{repetition} {parentId ? 'repeat or self replies' : 'posts'} in 10m</>,
|
||||
modifier: (cost) => cost * Math.pow(10, repetition)
|
||||
}
|
||||
})
|
||||
}, [data?.itemRepetition])
|
||||
|
||||
return line
|
||||
}
|
||||
}
|
||||
|
||||
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
|
||||
const [lineItems, setLineItems] = useState({})
|
||||
|
||||
const remoteLineItems = useRemoteLineItems()
|
||||
|
||||
const mergeLineItems = useCallback((newLineItems) => {
|
||||
setLineItems(lineItems => ({
|
||||
...lineItems,
|
||||
...newLineItems
|
||||
}))
|
||||
}, [setLineItems])
|
||||
|
||||
const value = useMemo(() => {
|
||||
const lines = { ...baseLineItems, ...lineItems, ...remoteLineItems }
|
||||
return {
|
||||
lines,
|
||||
merge: mergeLineItems,
|
||||
total: Object.values(lines).reduce((acc, { modifier }) => modifier(acc), 0)
|
||||
}
|
||||
}, [baseLineItems, lineItems, remoteLineItems, mergeLineItems])
|
||||
|
||||
return (
|
||||
<FeeButtonContext.Provider value={value}>
|
||||
{children}
|
||||
</FeeButtonContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useFeeButton () {
|
||||
return useContext(FeeButtonContext)
|
||||
}
|
||||
|
||||
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
|
||||
const me = useMe()
|
||||
const { lines, total } = useFeeButton()
|
||||
|
||||
return (
|
||||
<div className={styles.feeButton}>
|
||||
<ActionTooltip overlayText={numWithUnits(total, { abbreviate: false })}>
|
||||
<ChildButton variant={variant} disabled={disabled}>{text}{total > 1 && <small> {numWithUnits(total, { abbreviate: false, format: true })}</small>}</ChildButton>
|
||||
</ActionTooltip>
|
||||
{!me && <AnonInfo />}
|
||||
{total > 1 &&
|
||||
<Info>
|
||||
<Receipt lines={lines} total={total} />
|
||||
</Info>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Receipt ({ lines, total }) {
|
||||
return (
|
||||
<Table className={styles.receipt} borderless size='sm'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{numWithUnits(baseFee, { abbreviate: false })}</td>
|
||||
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
||||
</tr>
|
||||
{repetition > 0 &&
|
||||
<tr>
|
||||
<td>x 10<sup>{repetition}</sup></td>
|
||||
<td className='font-weight-light' align='right'>{repetition} {parentId ? 'repeat or self replies' : 'posts'} in 10m</td>
|
||||
</tr>}
|
||||
{imageFeesInfo.totalFees > 0 &&
|
||||
<tr>
|
||||
<td>+ {imageFeesInfo.nUnpaid} x {numWithUnits(imageFeesInfo.imageFee, { abbreviate: false })}</td>
|
||||
<td align='right' className='font-weight-light'>image fee</td>
|
||||
</tr>}
|
||||
{boost > 0 &&
|
||||
<tr>
|
||||
<td>+ {numWithUnits(boost, { abbreviate: false })}</td>
|
||||
<td className='font-weight-light' align='right'>boost</td>
|
||||
</tr>}
|
||||
{Object.entries(lines).map(([key, { term, label, omit }]) => (
|
||||
!omit &&
|
||||
<tr key={key}>
|
||||
<td>{term}</td>
|
||||
<td align='right' className='font-weight-light'>{label}</td>
|
||||
</tr>))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td className='fw-bold'>{numWithUnits(cost, { abbreviate: false })}</td>
|
||||
<td className='fw-bold'>{numWithUnits(total, { abbreviate: false, format: true })}</td>
|
||||
<td align='right' className='font-weight-light'>total fee</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
|
@ -68,92 +155,3 @@ function AnonInfo () {
|
|||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function FeeButton ({ parentId, baseFee, ChildButton, variant, text, alwaysShow, disabled }) {
|
||||
const me = useMe()
|
||||
baseFee = me ? baseFee : (parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
||||
const query = parentId
|
||||
? gql`{ itemRepetition(parentId: "${parentId}") }`
|
||||
: gql`{ itemRepetition }`
|
||||
const { data } = useQuery(query, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||
const repetition = me ? data?.itemRepetition || 0 : 0
|
||||
const formik = useFormikContext()
|
||||
const boost = Number(formik?.values?.boost) || 0
|
||||
const cost = baseFee * Math.pow(10, repetition) + Number(boost)
|
||||
|
||||
useEffect(() => {
|
||||
formik?.setFieldValue('cost', cost)
|
||||
}, [formik?.getFieldProps('cost').value, cost])
|
||||
|
||||
const imageFeesInfo = formik?.getFieldProps('imageFeesInfo').value || { totalFees: 0 }
|
||||
const totalCost = cost + imageFeesInfo.totalFees
|
||||
|
||||
const show = alwaysShow || !formik?.isSubmitting
|
||||
return (
|
||||
<div className={styles.feeButton}>
|
||||
<ActionTooltip overlayText={numWithUnits(totalCost, { abbreviate: false })}>
|
||||
<ChildButton variant={variant} disabled={disabled}>{text}{totalCost > 1 && show && <small> {numWithUnits(totalCost, { abbreviate: false })}</small>}</ChildButton>
|
||||
</ActionTooltip>
|
||||
{!me && <AnonInfo />}
|
||||
{totalCost > baseFee && show &&
|
||||
<Info>
|
||||
<Receipt baseFee={baseFee} imageFeesInfo={imageFeesInfo} repetition={repetition} cost={totalCost} parentId={parentId} boost={boost} />
|
||||
</Info>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EditReceipt ({ cost, paidSats, imageFeesInfo, boost, parentId }) {
|
||||
return (
|
||||
<Table className={styles.receipt} borderless size='sm'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{numWithUnits(0, { abbreviate: false })}</td>
|
||||
<td align='right' className='font-weight-light'>edit fee</td>
|
||||
</tr>
|
||||
{imageFeesInfo.totalFees > 0 &&
|
||||
<tr>
|
||||
<td>+ {imageFeesInfo.nUnpaid} x {numWithUnits(imageFeesInfo.imageFee, { abbreviate: false })}</td>
|
||||
<td align='right' className='font-weight-light'>image fee</td>
|
||||
</tr>}
|
||||
{boost > 0 &&
|
||||
<tr>
|
||||
<td>+ {numWithUnits(boost, { abbreviate: false })}</td>
|
||||
<td className='font-weight-light' align='right'>boost</td>
|
||||
</tr>}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td className='fw-bold'>{numWithUnits(cost)}</td>
|
||||
<td align='right' className='font-weight-light'>total fee</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
export function EditFeeButton ({ paidSats, ChildButton, variant, text, alwaysShow, parentId }) {
|
||||
const formik = useFormikContext()
|
||||
const boost = (formik?.values?.boost || 0) - (formik?.initialValues?.boost || 0)
|
||||
const cost = Number(boost)
|
||||
|
||||
useEffect(() => {
|
||||
formik?.setFieldValue('cost', cost)
|
||||
}, [formik?.getFieldProps('cost').value, cost])
|
||||
|
||||
const imageFeesInfo = formik?.getFieldProps('imageFeesInfo').value || { totalFees: 0 }
|
||||
const totalCost = cost + imageFeesInfo.totalFees
|
||||
|
||||
const show = alwaysShow || !formik?.isSubmitting
|
||||
return (
|
||||
<div className='d-flex align-items-center'>
|
||||
<ActionTooltip overlayText={numWithUnits(totalCost >= 0 ? totalCost : 0, { abbreviate: false })}>
|
||||
<ChildButton variant={variant}>{text}{totalCost > 0 && show && <small> {numWithUnits(totalCost, { abbreviate: false })}</small>}</ChildButton>
|
||||
</ActionTooltip>
|
||||
{totalCost > 0 && show &&
|
||||
<Info>
|
||||
<EditReceipt paidSats={paidSats} imageFeesInfo={imageFeesInfo} cost={totalCost} parentId={parentId} boost={boost} />
|
||||
</Info>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -23,20 +23,17 @@ import { numWithUnits } from '../lib/format'
|
|||
import textAreaCaret from 'textarea-caret'
|
||||
import ReactDatePicker from 'react-datepicker'
|
||||
import 'react-datepicker/dist/react-datepicker.css'
|
||||
import { debounce } from './use-debounce-callback'
|
||||
import useDebounceCallback, { debounce } from './use-debounce-callback'
|
||||
import { ImageUpload } from './image'
|
||||
import { AWS_S3_URL_REGEXP } from '../lib/constants'
|
||||
import { dayMonthYear, dayMonthYearToDate, whenToFrom } from '../lib/time'
|
||||
import { useFeeButton } from './fee-button'
|
||||
import Thumb from '../svgs/thumb-up-fill.svg'
|
||||
|
||||
export function SubmitButton ({
|
||||
children, variant, value, onClick, disabled, cost, ...props
|
||||
children, variant, value, onClick, disabled, ...props
|
||||
}) {
|
||||
const formik = useFormikContext()
|
||||
useEffect(() => {
|
||||
if (cost) {
|
||||
formik?.setFieldValue('cost', cost)
|
||||
}
|
||||
}, [formik?.setFieldValue, formik?.getFieldProps('cost').value, cost])
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
@ -58,11 +55,14 @@ export function SubmitButton ({
|
|||
|
||||
export function CopyInput (props) {
|
||||
const toaster = useToast()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleClick = async () => {
|
||||
try {
|
||||
await copy(props.placeholder)
|
||||
toaster.success('copied')
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
} catch (err) {
|
||||
toaster.danger('failed to copy')
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ export function CopyInput (props) {
|
|||
className={styles.appendButton}
|
||||
size={props.size}
|
||||
onClick={handleClick}
|
||||
>copy
|
||||
>{copied ? <Thumb width={18} height={18} /> : 'copy'}
|
||||
</Button>
|
||||
}
|
||||
{...props}
|
||||
|
@ -105,7 +105,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
innerRef = innerRef || useRef(null)
|
||||
const imageUploadRef = useRef(null)
|
||||
const previousTab = useRef(tab)
|
||||
const formik = useFormikContext()
|
||||
const { merge } = useFeeButton()
|
||||
const toaster = useToast()
|
||||
const [updateImageFeesInfo] = useLazyQuery(gql`
|
||||
query imageFeesInfo($s3Keys: [Int]!) {
|
||||
|
@ -123,7 +123,14 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
toaster.danger(err.message || err.toString?.())
|
||||
},
|
||||
onCompleted: ({ imageFeesInfo }) => {
|
||||
formik?.setFieldValue('imageFeesInfo', imageFeesInfo)
|
||||
merge({
|
||||
imageFee: {
|
||||
term: `+ ${numWithUnits(imageFeesInfo.totalFees, { abbreviate: false })}`,
|
||||
label: 'image fee',
|
||||
modifier: cost => cost + imageFeesInfo.totalFees,
|
||||
omit: !imageFeesInfo.totalFees
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -164,11 +171,11 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||
innerRef.current.focus()
|
||||
}, [mentionIndices, innerRef, helpers?.setValue])
|
||||
|
||||
const imageFeesUpdate = useCallback(debounce(
|
||||
const imageFeesUpdate = useDebounceCallback(
|
||||
(text) => {
|
||||
const s3Keys = text ? [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])) : []
|
||||
updateImageFeesInfo({ variables: { s3Keys } })
|
||||
}, 1000), [debounce, updateImageFeesInfo])
|
||||
}, 1000, [updateImageFeesInfo])
|
||||
|
||||
const onChangeInner = useCallback((formik, e) => {
|
||||
if (onChange) onChange(formik, e)
|
||||
|
@ -426,6 +433,7 @@ function InputInner ({
|
|||
// for some reason we have to turn off validation to get formik to
|
||||
// not assume this is invalid
|
||||
helpers.setValue(draft, false)
|
||||
onChange && onChange(formik, { target: { value: draft } })
|
||||
}
|
||||
}
|
||||
}, [overrideValue])
|
||||
|
@ -720,6 +728,7 @@ export function Form ({
|
|||
}) {
|
||||
const toaster = useToast()
|
||||
const initialErrorToasted = useRef(false)
|
||||
const feeButton = useFeeButton()
|
||||
useEffect(() => {
|
||||
if (initialError && !initialErrorToasted.current) {
|
||||
toaster.danger(initialError.message || initialError.toString?.())
|
||||
|
@ -757,11 +766,8 @@ export function Form ({
|
|||
if (onSubmit) {
|
||||
// extract cost from formik fields
|
||||
// (cost may also be set in a formik field named 'amount')
|
||||
let cost = values?.cost || values?.amount
|
||||
const cost = feeButton?.total || values?.amount
|
||||
if (cost) {
|
||||
// add potential image fees which are set in a different field
|
||||
// to differentiate between fees (in receipts for example)
|
||||
cost += (values?.imageFeesInfo?.totalFees || 0)
|
||||
values.cost = cost
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Checkbox, Form, Input, MarkdownInput, SubmitButton } from './form'
|
||||
import { Checkbox, Form, Input, MarkdownInput } from './form'
|
||||
import Row from 'react-bootstrap/Row'
|
||||
import Col from 'react-bootstrap/Col'
|
||||
import InputGroup from 'react-bootstrap/InputGroup'
|
||||
|
@ -14,10 +14,10 @@ import { useRouter } from 'next/router'
|
|||
import Link from 'next/link'
|
||||
import { usePrice } from './price'
|
||||
import Avatar from './avatar'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import { jobSchema } from '../lib/validate'
|
||||
import CancelButton from './cancel-button'
|
||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||
import FeeButton from './fee-button'
|
||||
|
||||
function satsMin2Mo (minute) {
|
||||
return minute * 30 * 24 * 60
|
||||
|
@ -156,18 +156,11 @@ export default function JobForm ({ item, sub }) {
|
|||
<PromoteJob item={item} sub={sub} />
|
||||
{item && <StatusControl item={item} />}
|
||||
<div className='d-flex align-items-center justify-content-end mt-3'>
|
||||
{item
|
||||
? (
|
||||
<div className='d-flex'>
|
||||
<CancelButton />
|
||||
<SubmitButton variant='secondary'>save</SubmitButton>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<ActionTooltip overlayText='1000 sats'>
|
||||
<SubmitButton cost={1000} variant='secondary'>post <small> 1000 sats</small></SubmitButton>
|
||||
</ActionTooltip>
|
||||
)}
|
||||
<FeeButton
|
||||
text={item ? 'save' : 'post'}
|
||||
variant='secondary'
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import { Form, Input, MarkdownInput } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||
import Countdown from './countdown'
|
||||
|
@ -7,7 +7,7 @@ import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
|||
import { ITEM_FIELDS } from '../fragments/items'
|
||||
import Item from './item'
|
||||
import AccordianItem from './accordian-item'
|
||||
import FeeButton, { EditFeeButton } from './fee-button'
|
||||
import FeeButton from './fee-button'
|
||||
import Delete from './delete'
|
||||
import Button from 'react-bootstrap/Button'
|
||||
import { linkSchema } from '../lib/validate'
|
||||
|
@ -194,33 +194,24 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||
/>
|
||||
</AdvPostForm>
|
||||
<div className='mt-3'>
|
||||
<div className='d-flex justify-content-between'>
|
||||
{item
|
||||
? (
|
||||
<div className='d-flex justify-content-between'>
|
||||
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
|
||||
<Button variant='grey-medium'>delete</Button>
|
||||
</Delete>
|
||||
<div className='d-flex'>
|
||||
<CancelButton />
|
||||
<EditFeeButton
|
||||
paidSats={item.meSats}
|
||||
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
: (
|
||||
<div className='d-flex align-items-center'>
|
||||
<FeeButton
|
||||
baseFee={1} parentId={null} text='post' disabled={postDisabled}
|
||||
ChildButton={SubmitButton} variant='secondary'
|
||||
/>
|
||||
{dupesLoading &&
|
||||
<div className='d-flex ms-3 justify-content-center'>
|
||||
</Delete>)
|
||||
: dupesLoading &&
|
||||
<div className='d-flex justify-content-center'>
|
||||
<Moon className='spin fill-grey' />
|
||||
<div className='ms-2 text-muted' style={{ fontWeight: '600' }}>searching for dupes</div>
|
||||
</div>}
|
||||
<div className='d-flex align-items-center ms-auto'>
|
||||
<CancelButton />
|
||||
<FeeButton
|
||||
text={item ? 'save' : 'post'} disabled={postDisabled} variant='secondary'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!item &&
|
||||
<>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { Form, Input, MarkdownInput, SubmitButton, VariableInput } from '../components/form'
|
||||
import { Form, Input, MarkdownInput, VariableInput } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
||||
import { MAX_POLL_CHOICE_LENGTH, MAX_POLL_NUM_CHOICES, MAX_TITLE_LENGTH } from '../lib/constants'
|
||||
import FeeButton, { EditFeeButton } from './fee-button'
|
||||
import FeeButton from './fee-button'
|
||||
import Delete from './delete'
|
||||
import Button from 'react-bootstrap/Button'
|
||||
import { pollSchema } from '../lib/validate'
|
||||
|
@ -99,24 +99,20 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||
/>
|
||||
<AdvPostForm edit={!!item} />
|
||||
<div className='mt-3'>
|
||||
{item
|
||||
? (
|
||||
<div className='mt-3'>
|
||||
<div className='d-flex justify-content-between'>
|
||||
{item &&
|
||||
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
|
||||
<Button variant='grey-medium'>delete</Button>
|
||||
</Delete>
|
||||
<div className='d-flex'>
|
||||
</Delete>}
|
||||
<div className='d-flex align-items-center ms-auto'>
|
||||
<CancelButton />
|
||||
<EditFeeButton
|
||||
paidSats={item.meSats}
|
||||
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
|
||||
<FeeButton
|
||||
text={item ? 'save' : 'post'} variant='secondary'
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
: <FeeButton
|
||||
baseFee={1} parentId={null} text='post'
|
||||
ChildButton={SubmitButton} variant='secondary'
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)
|
||||
|
|
|
@ -12,6 +12,7 @@ import { BountyForm } from './bounty-form'
|
|||
import SubSelect from './sub-select-form'
|
||||
import Info from './info'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
|
||||
|
||||
function FreebieDialog () {
|
||||
return (
|
||||
|
@ -94,7 +95,14 @@ export function PostForm ({ type, sub, children }) {
|
|||
FormType = BountyForm
|
||||
}
|
||||
|
||||
return <FormType sub={sub}>{children}</FormType>
|
||||
return (
|
||||
<FeeButtonProvider
|
||||
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, me: !!me }) : undefined}
|
||||
useRemoteLineItems={postCommentUseRemoteLineItems({ me: !!me })}
|
||||
>
|
||||
<FormType sub={sub}>{children}</FormType>
|
||||
</FeeButtonProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Post ({ sub }) {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { Form, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import { Form, MarkdownInput } from '../components/form'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import styles from './reply.module.css'
|
||||
import { COMMENTS } from '../fragments/comments'
|
||||
import { useMe } from './me'
|
||||
import { forwardRef, useCallback, useEffect, useState, useRef, useImperativeHandle } from 'react'
|
||||
import Link from 'next/link'
|
||||
import FeeButton from './fee-button'
|
||||
import FeeButton, { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
|
||||
import { commentsViewedAfterComment } from '../lib/new-comments'
|
||||
import { commentSchema } from '../lib/validate'
|
||||
import Info from './info'
|
||||
|
@ -183,7 +183,12 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
|
|||
{/* HACK if we need more items, we should probably do a comment toolbar */}
|
||||
{children}
|
||||
</div>)}
|
||||
<div className={styles.reply} style={{ display: reply ? 'block' : 'none' }}>
|
||||
{reply &&
|
||||
<div className={styles.reply}>
|
||||
<FeeButtonProvider
|
||||
baseLineItems={postCommentBaseLineItems({ baseCost: 1, comment: true, me: !!me })}
|
||||
useRemoteLineItems={postCommentUseRemoteLineItems({ parentId: item.id, me: !!me })}
|
||||
>
|
||||
<Form
|
||||
initial={{
|
||||
text: ''
|
||||
|
@ -203,16 +208,19 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
|
|||
hint={me?.sats < 1 && <FreebieDialog />}
|
||||
innerRef={replyInput}
|
||||
/>
|
||||
{reply &&
|
||||
<div className='mt-1'>
|
||||
<div className='d-flex mt-1'>
|
||||
<div className='ms-auto'>
|
||||
<FeeButton
|
||||
baseFee={1} parentId={parentId} text='reply'
|
||||
ChildButton={SubmitButton} variant='secondary' alwaysShow
|
||||
text='reply'
|
||||
variant='secondary'
|
||||
alwaysShow
|
||||
/>
|
||||
</div>}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</FeeButtonProvider>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -29,11 +29,18 @@ function getClient (uri) {
|
|||
return new ApolloClient({
|
||||
link: new HttpLink({ uri }),
|
||||
ssrMode: SSR,
|
||||
connectToDevTools: process.env.NODE_ENV !== 'production',
|
||||
cache: new InMemoryCache({
|
||||
freezeResults: true,
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
sub: {
|
||||
keyArgs: ['name'],
|
||||
merge (existing, incoming) {
|
||||
return incoming
|
||||
}
|
||||
},
|
||||
topUsers: {
|
||||
keyArgs: ['when', 'by', 'from', 'to', 'limit'],
|
||||
merge (existing, incoming) {
|
||||
|
|
|
@ -5,12 +5,12 @@ import Button from 'react-bootstrap/Button'
|
|||
import styles from '../../styles/user.module.css'
|
||||
import { useState } from 'react'
|
||||
import ItemFull from '../../components/item-full'
|
||||
import { Form, MarkdownInput, SubmitButton } from '../../components/form'
|
||||
import { Form, MarkdownInput } from '../../components/form'
|
||||
import { useMe } from '../../components/me'
|
||||
import { USER_FULL } from '../../fragments/users'
|
||||
import { ITEM_FIELDS } from '../../fragments/items'
|
||||
import { getGetServerSideProps } from '../../api/ssrApollo'
|
||||
import FeeButton, { EditFeeButton } from '../../components/fee-button'
|
||||
import FeeButton, { FeeButtonProvider } from '../../components/fee-button'
|
||||
import { bioSchema } from '../../lib/validate'
|
||||
import CancelButton from '../../components/cancel-button'
|
||||
import { useRouter } from 'next/router'
|
||||
|
@ -69,15 +69,9 @@ export function BioForm ({ handleDone, bio }) {
|
|||
/>
|
||||
<div className='d-flex mt-3 justify-content-end'>
|
||||
<CancelButton onClick={handleDone} />
|
||||
{bio?.text
|
||||
? <EditFeeButton
|
||||
paidSats={bio?.meSats}
|
||||
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
|
||||
/>
|
||||
: <FeeButton
|
||||
baseFee={1} parentId={null} text='create'
|
||||
ChildButton={SubmitButton} variant='secondary'
|
||||
/>}
|
||||
<FeeButtonProvider>
|
||||
<FeeButton text='save' variant='secondary' />
|
||||
</FeeButtonProvider>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,7 @@ import { useState } from 'react'
|
|||
import { useQuery } from '@apollo/client'
|
||||
import { useRouter } from 'next/router'
|
||||
import PageLoading from '../../../components/page-loading'
|
||||
import { FeeButtonProvider } from '../../../components/fee-button'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({
|
||||
query: ITEM,
|
||||
|
@ -39,11 +40,23 @@ export default function PostEdit ({ ssrData }) {
|
|||
FormType = BountyForm
|
||||
}
|
||||
|
||||
const existingBoostLineItem = item.boost
|
||||
? {
|
||||
existingBoost: {
|
||||
label: 'old boost',
|
||||
term: `- ${item.boost}`,
|
||||
modifier: cost => cost - item.boost
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<CenterLayout sub={sub}>
|
||||
<FeeButtonProvider baseLineItems={existingBoostLineItem}>
|
||||
<FormType item={item} editThreshold={editThreshold}>
|
||||
{!item.isJob && <SubSelect label='sub' item={item} setSub={setSub} sub={sub} />}
|
||||
</FormType>
|
||||
</FeeButtonProvider>
|
||||
</CenterLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ export function DonateButton () {
|
|||
<Button onClick={() => showModal(onClose => (
|
||||
<Form
|
||||
initial={{
|
||||
amount: 1000
|
||||
amount: 10000
|
||||
}}
|
||||
schema={amountSchema}
|
||||
invoiceable
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
-- add base cost
|
||||
CREATE OR REPLACE FUNCTION create_item(
|
||||
jitem JSONB, forward JSONB, poll_options JSONB, spam_within INTERVAL, upload_ids INTEGER[])
|
||||
RETURNS "Item"
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_msats BIGINT;
|
||||
cost_msats BIGINT := 1000;
|
||||
base_cost_msats BIGINT;
|
||||
freebie BOOLEAN;
|
||||
item "Item";
|
||||
med_votes FLOAT;
|
||||
select_clause TEXT;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
-- access fields with appropriate types
|
||||
item := jsonb_populate_record(NULL::"Item", jitem);
|
||||
|
||||
SELECT msats INTO user_msats FROM users WHERE id = item."userId";
|
||||
|
||||
-- if this is a post, get the base cost of the sub
|
||||
IF item."parentId" IS NULL THEN
|
||||
SELECT "baseCost" * 1000, "baseCost" * 1000
|
||||
INTO base_cost_msats, cost_msats
|
||||
FROM "Sub"
|
||||
WHERE name = item."subName";
|
||||
END IF;
|
||||
|
||||
IF item."maxBid" IS NULL THEN
|
||||
-- spam multiplier
|
||||
cost_msats := cost_msats * POWER(10, item_spam(item."parentId", item."userId", spam_within));
|
||||
END IF;
|
||||
|
||||
-- add image fees
|
||||
IF upload_ids IS NOT NULL THEN
|
||||
cost_msats := cost_msats + (SELECT "nUnpaid" * "imageFeeMsats" FROM image_fees_info(item."userId", upload_ids));
|
||||
UPDATE "Upload" SET paid = 't' WHERE id = ANY(upload_ids);
|
||||
END IF;
|
||||
|
||||
-- it's only a freebie if it's no greater than the base cost, they have less than the cost, and boost = 0
|
||||
freebie := (cost_msats <= base_cost_msats) AND (user_msats < cost_msats) AND (item.boost IS NULL OR item.boost = 0);
|
||||
|
||||
IF NOT freebie AND cost_msats > user_msats THEN
|
||||
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||
END IF;
|
||||
|
||||
-- get this user's median item score
|
||||
SELECT COALESCE(
|
||||
percentile_cont(0.5) WITHIN GROUP(
|
||||
ORDER BY "weightedVotes" - "weightedDownVotes"), 0)
|
||||
INTO med_votes FROM "Item" WHERE "userId" = item."userId";
|
||||
|
||||
-- if their median votes are positive, start at 0
|
||||
-- if the median votes are negative, start their post with that many down votes
|
||||
-- basically: if their median post is bad, presume this post is too
|
||||
-- addendum: if they're an anon poster, always start at 0
|
||||
IF med_votes >= 0 OR item."userId" = 27 THEN
|
||||
med_votes := 0;
|
||||
ELSE
|
||||
med_votes := ABS(med_votes);
|
||||
END IF;
|
||||
|
||||
-- there's no great way to set default column values when using json_populate_record
|
||||
-- so we need to only select fields with non-null values that way when func input
|
||||
-- does not include a value, the default value is used instead of null
|
||||
SELECT string_agg(quote_ident(key), ',') INTO select_clause
|
||||
FROM jsonb_object_keys(jsonb_strip_nulls(jitem)) k(key);
|
||||
-- insert the item
|
||||
EXECUTE format($fmt$
|
||||
INSERT INTO "Item" (%s, "weightedDownVotes", freebie)
|
||||
SELECT %1$s, %L, %L
|
||||
FROM jsonb_populate_record(NULL::"Item", %L) RETURNING *
|
||||
$fmt$, select_clause, med_votes, freebie, jitem) INTO item;
|
||||
|
||||
INSERT INTO "ItemForward" ("itemId", "userId", "pct")
|
||||
SELECT item.id, "userId", "pct" FROM jsonb_populate_recordset(NULL::"ItemForward", forward);
|
||||
|
||||
-- Automatically subscribe to one's own posts
|
||||
INSERT INTO "ThreadSubscription" ("itemId", "userId")
|
||||
VALUES (item.id, item."userId");
|
||||
|
||||
-- Automatically subscribe forward recipients to the new post
|
||||
INSERT INTO "ThreadSubscription" ("itemId", "userId")
|
||||
SELECT item.id, "userId" FROM jsonb_populate_recordset(NULL::"ItemForward", forward);
|
||||
|
||||
INSERT INTO "PollOption" ("itemId", "option")
|
||||
SELECT item.id, "option" FROM jsonb_array_elements_text(poll_options) o("option");
|
||||
|
||||
IF NOT freebie THEN
|
||||
UPDATE users SET msats = msats - cost_msats WHERE id = item."userId";
|
||||
|
||||
INSERT INTO "ItemAct" (msats, "itemId", "userId", act)
|
||||
VALUES (cost_msats, item.id, item."userId", 'FEE');
|
||||
END IF;
|
||||
|
||||
-- if this item has boost
|
||||
IF item.boost > 0 THEN
|
||||
PERFORM item_act(item.id, item."userId", 'BOOST', item.boost);
|
||||
END IF;
|
||||
|
||||
-- if this is a job
|
||||
IF item."maxBid" IS NOT NULL THEN
|
||||
PERFORM run_auction(item.id);
|
||||
END IF;
|
||||
|
||||
-- if this is a bio
|
||||
IF item.bio THEN
|
||||
UPDATE users SET "bioId" = item.id WHERE id = item."userId";
|
||||
END IF;
|
||||
|
||||
-- record attachments
|
||||
IF upload_ids IS NOT NULL THEN
|
||||
INSERT INTO "ItemUpload" ("itemId", "uploadId")
|
||||
SELECT item.id, * FROM UNNEST(upload_ids);
|
||||
END IF;
|
||||
|
||||
-- schedule imgproxy job
|
||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
|
||||
VALUES ('imgproxy', jsonb_build_object('id', item.id), 21, true, now() + interval '5 seconds');
|
||||
|
||||
RETURN item;
|
||||
END;
|
||||
$$;
|
||||
|
||||
UPDATE "Sub" SET "baseCost" = 10 WHERE name <> 'jobs';
|
||||
UPDATE "Sub" SET "baseCost" = 10000 WHERE name = 'jobs';
|
Loading…
Reference in New Issue