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 styles from './adv-post-form.module.css'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
import { useFeeButton } from './fee-button'
|
||||||
|
|
||||||
const EMPTY_FORWARD = { nym: '', pct: '' }
|
const EMPTY_FORWARD = { nym: '', pct: '' }
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ export function AdvPostInitial ({ forward, boost }) {
|
|||||||
export default function AdvPostForm ({ children }) {
|
export default function AdvPostForm ({ children }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { merge } = useFeeButton()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccordianItem
|
<AccordianItem
|
||||||
@ -51,6 +53,13 @@ export default function AdvPostForm ({ children }) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
name='boost'
|
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>}
|
hint={<span className='text-muted'>ranks posts higher temporarily based on the amount</span>}
|
||||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
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 { useRouter } from 'next/router'
|
||||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||||
import Countdown from './countdown'
|
import Countdown from './countdown'
|
||||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
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 InputGroup from 'react-bootstrap/InputGroup'
|
||||||
import { bountySchema } from '../lib/validate'
|
import { bountySchema } from '../lib/validate'
|
||||||
import { SubSelectInitial } from './sub-select-form'
|
import { SubSelectInitial } from './sub-select-form'
|
||||||
@ -133,29 +133,12 @@ export function BountyForm ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<AdvPostForm edit={!!item} />
|
<AdvPostForm edit={!!item} />
|
||||||
<div className='mt-3'>
|
<div className='d-flex mt-3'>
|
||||||
{item
|
<CancelButton />
|
||||||
? (
|
<FeeButton
|
||||||
<div className='d-flex'>
|
text={buttonText}
|
||||||
<CancelButton />
|
variant='secondary'
|
||||||
<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>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
@ -4,6 +4,6 @@ import Button from 'react-bootstrap/Button'
|
|||||||
export default function CancelButton ({ onClick }) {
|
export default function CancelButton ({ onClick }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
return (
|
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 { gql, useMutation } from '@apollo/client'
|
||||||
import styles from './reply.module.css'
|
import styles from './reply.module.css'
|
||||||
import { EditFeeButton } from './fee-button'
|
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import Delete from './delete'
|
import Delete from './delete'
|
||||||
import { commentSchema } from '../lib/validate'
|
import { commentSchema } from '../lib/validate'
|
||||||
|
import FeeButton, { FeeButtonProvider } from './fee-button'
|
||||||
|
|
||||||
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
|
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
|
||||||
const [upsertComment] = useMutation(
|
const [upsertComment] = useMutation(
|
||||||
@ -29,37 +29,41 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.reply} mt-2`}>
|
<div className={`${styles.reply} mt-2`}>
|
||||||
<Form
|
<FeeButtonProvider>
|
||||||
initial={{
|
<Form
|
||||||
text: comment.text
|
initial={{
|
||||||
}}
|
text: comment.text
|
||||||
schema={commentSchema}
|
}}
|
||||||
onSubmit={async (values, { resetForm }) => {
|
schema={commentSchema}
|
||||||
const { error } = await upsertComment({ variables: { ...values, id: comment.id } })
|
onSubmit={async (values, { resetForm }) => {
|
||||||
if (error) {
|
const { error } = await upsertComment({ variables: { ...values, id: comment.id } })
|
||||||
throw new Error({ message: error.toString() })
|
if (error) {
|
||||||
}
|
throw new Error({ message: error.toString() })
|
||||||
if (onSuccess) {
|
}
|
||||||
onSuccess()
|
if (onSuccess) {
|
||||||
}
|
onSuccess()
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
<MarkdownInput
|
>
|
||||||
name='text'
|
<MarkdownInput
|
||||||
minRows={6}
|
name='text'
|
||||||
autoFocus
|
minRows={6}
|
||||||
required
|
autoFocus
|
||||||
/>
|
required
|
||||||
<div className='d-flex justify-content-between'>
|
|
||||||
<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>
|
<div className='d-flex justify-content-between'>
|
||||||
</Form>
|
<Delete itemId={comment.id} onDelete={onSuccess} type='comment'>
|
||||||
|
<Button variant='grey-medium'>delete</Button>
|
||||||
|
</Delete>
|
||||||
|
<div className='d-flex mt-3'>
|
||||||
|
<FeeButton
|
||||||
|
text='save'
|
||||||
|
variant='secondary'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</FeeButtonProvider>
|
||||||
</div>
|
</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 { useRouter } from 'next/router'
|
||||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||||
import Countdown from './countdown'
|
import Countdown from './countdown'
|
||||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
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 { ITEM_FIELDS } from '../fragments/items'
|
||||||
import AccordianItem from './accordian-item'
|
import AccordianItem from './accordian-item'
|
||||||
import Item from './item'
|
import Item from './item'
|
||||||
@ -134,24 +134,19 @@ export function DiscussionForm ({
|
|||||||
/>
|
/>
|
||||||
<AdvPostForm edit={!!item} />
|
<AdvPostForm edit={!!item} />
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
{item
|
<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}`)}>
|
||||||
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
|
<Button variant='grey-medium'>delete</Button>
|
||||||
<Button variant='grey-medium'>delete</Button>
|
</Delete>}
|
||||||
</Delete>
|
<div className='d-flex align-items-center ms-auto'>
|
||||||
<div className='d-flex'>
|
<CancelButton />
|
||||||
<CancelButton />
|
<FeeButton
|
||||||
<EditFeeButton
|
text={buttonText}
|
||||||
paidSats={item.meSats}
|
variant='secondary'
|
||||||
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
|
||||||
: <FeeButton
|
|
||||||
baseFee={1} parentId={null} text={buttonText}
|
|
||||||
ChildButton={SubmitButton} variant='secondary'
|
|
||||||
/>}
|
|
||||||
</div>
|
</div>
|
||||||
{!item &&
|
{!item &&
|
||||||
<div className={`mt-3 ${related.length > 0 ? '' : 'invisible'}`}>
|
<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 Table from 'react-bootstrap/Table'
|
||||||
import ActionTooltip from './action-tooltip'
|
import ActionTooltip from './action-tooltip'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
import styles from './fee-button.module.css'
|
import styles from './fee-button.module.css'
|
||||||
import { gql, useQuery } from '@apollo/client'
|
import { gql, useQuery } from '@apollo/client'
|
||||||
import { useFormikContext } from 'formik'
|
import { SSR } from '../lib/constants'
|
||||||
import { SSR, ANON_COMMENT_FEE, ANON_POST_FEE } from '../lib/constants'
|
|
||||||
import { numWithUnits } from '../lib/format'
|
import { numWithUnits } from '../lib/format'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import AnonIcon from '../svgs/spy-fill.svg'
|
import AnonIcon from '../svgs/spy-fill.svg'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
import Link from 'next/link'
|
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 (
|
return (
|
||||||
<Table className={styles.receipt} borderless size='sm'>
|
<Table className={styles.receipt} borderless size='sm'>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
{Object.entries(lines).map(([key, { term, label, omit }]) => (
|
||||||
<td>{numWithUnits(baseFee, { abbreviate: false })}</td>
|
!omit &&
|
||||||
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
<tr key={key}>
|
||||||
</tr>
|
<td>{term}</td>
|
||||||
{repetition > 0 &&
|
<td align='right' className='font-weight-light'>{label}</td>
|
||||||
<tr>
|
</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>}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr>
|
<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>
|
<td align='right' className='font-weight-light'>total fee</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tfoot>
|
</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 textAreaCaret from 'textarea-caret'
|
||||||
import ReactDatePicker from 'react-datepicker'
|
import ReactDatePicker from 'react-datepicker'
|
||||||
import 'react-datepicker/dist/react-datepicker.css'
|
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 { ImageUpload } from './image'
|
||||||
import { AWS_S3_URL_REGEXP } from '../lib/constants'
|
import { AWS_S3_URL_REGEXP } from '../lib/constants'
|
||||||
import { dayMonthYear, dayMonthYearToDate, whenToFrom } from '../lib/time'
|
import { dayMonthYear, dayMonthYearToDate, whenToFrom } from '../lib/time'
|
||||||
|
import { useFeeButton } from './fee-button'
|
||||||
|
import Thumb from '../svgs/thumb-up-fill.svg'
|
||||||
|
|
||||||
export function SubmitButton ({
|
export function SubmitButton ({
|
||||||
children, variant, value, onClick, disabled, cost, ...props
|
children, variant, value, onClick, disabled, ...props
|
||||||
}) {
|
}) {
|
||||||
const formik = useFormikContext()
|
const formik = useFormikContext()
|
||||||
useEffect(() => {
|
|
||||||
if (cost) {
|
|
||||||
formik?.setFieldValue('cost', cost)
|
|
||||||
}
|
|
||||||
}, [formik?.setFieldValue, formik?.getFieldProps('cost').value, cost])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -58,11 +55,14 @@ export function SubmitButton ({
|
|||||||
|
|
||||||
export function CopyInput (props) {
|
export function CopyInput (props) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
try {
|
try {
|
||||||
await copy(props.placeholder)
|
await copy(props.placeholder)
|
||||||
toaster.success('copied')
|
toaster.success('copied')
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 1500)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toaster.danger('failed to copy')
|
toaster.danger('failed to copy')
|
||||||
}
|
}
|
||||||
@ -76,7 +76,7 @@ export function CopyInput (props) {
|
|||||||
className={styles.appendButton}
|
className={styles.appendButton}
|
||||||
size={props.size}
|
size={props.size}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>copy
|
>{copied ? <Thumb width={18} height={18} /> : 'copy'}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
@ -105,7 +105,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||||||
innerRef = innerRef || useRef(null)
|
innerRef = innerRef || useRef(null)
|
||||||
const imageUploadRef = useRef(null)
|
const imageUploadRef = useRef(null)
|
||||||
const previousTab = useRef(tab)
|
const previousTab = useRef(tab)
|
||||||
const formik = useFormikContext()
|
const { merge } = useFeeButton()
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const [updateImageFeesInfo] = useLazyQuery(gql`
|
const [updateImageFeesInfo] = useLazyQuery(gql`
|
||||||
query imageFeesInfo($s3Keys: [Int]!) {
|
query imageFeesInfo($s3Keys: [Int]!) {
|
||||||
@ -123,7 +123,14 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
|||||||
toaster.danger(err.message || err.toString?.())
|
toaster.danger(err.message || err.toString?.())
|
||||||
},
|
},
|
||||||
onCompleted: ({ imageFeesInfo }) => {
|
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()
|
innerRef.current.focus()
|
||||||
}, [mentionIndices, innerRef, helpers?.setValue])
|
}, [mentionIndices, innerRef, helpers?.setValue])
|
||||||
|
|
||||||
const imageFeesUpdate = useCallback(debounce(
|
const imageFeesUpdate = useDebounceCallback(
|
||||||
(text) => {
|
(text) => {
|
||||||
const s3Keys = text ? [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])) : []
|
const s3Keys = text ? [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1])) : []
|
||||||
updateImageFeesInfo({ variables: { s3Keys } })
|
updateImageFeesInfo({ variables: { s3Keys } })
|
||||||
}, 1000), [debounce, updateImageFeesInfo])
|
}, 1000, [updateImageFeesInfo])
|
||||||
|
|
||||||
const onChangeInner = useCallback((formik, e) => {
|
const onChangeInner = useCallback((formik, e) => {
|
||||||
if (onChange) onChange(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
|
// for some reason we have to turn off validation to get formik to
|
||||||
// not assume this is invalid
|
// not assume this is invalid
|
||||||
helpers.setValue(draft, false)
|
helpers.setValue(draft, false)
|
||||||
|
onChange && onChange(formik, { target: { value: draft } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [overrideValue])
|
}, [overrideValue])
|
||||||
@ -720,6 +728,7 @@ export function Form ({
|
|||||||
}) {
|
}) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const initialErrorToasted = useRef(false)
|
const initialErrorToasted = useRef(false)
|
||||||
|
const feeButton = useFeeButton()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialError && !initialErrorToasted.current) {
|
if (initialError && !initialErrorToasted.current) {
|
||||||
toaster.danger(initialError.message || initialError.toString?.())
|
toaster.danger(initialError.message || initialError.toString?.())
|
||||||
@ -757,11 +766,8 @@ export function Form ({
|
|||||||
if (onSubmit) {
|
if (onSubmit) {
|
||||||
// extract cost from formik fields
|
// extract cost from formik fields
|
||||||
// (cost may also be set in a formik field named 'amount')
|
// (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) {
|
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
|
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 Row from 'react-bootstrap/Row'
|
||||||
import Col from 'react-bootstrap/Col'
|
import Col from 'react-bootstrap/Col'
|
||||||
import InputGroup from 'react-bootstrap/InputGroup'
|
import InputGroup from 'react-bootstrap/InputGroup'
|
||||||
@ -14,10 +14,10 @@ import { useRouter } from 'next/router'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePrice } from './price'
|
import { usePrice } from './price'
|
||||||
import Avatar from './avatar'
|
import Avatar from './avatar'
|
||||||
import ActionTooltip from './action-tooltip'
|
|
||||||
import { jobSchema } from '../lib/validate'
|
import { jobSchema } from '../lib/validate'
|
||||||
import CancelButton from './cancel-button'
|
import CancelButton from './cancel-button'
|
||||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||||
|
import FeeButton from './fee-button'
|
||||||
|
|
||||||
function satsMin2Mo (minute) {
|
function satsMin2Mo (minute) {
|
||||||
return minute * 30 * 24 * 60
|
return minute * 30 * 24 * 60
|
||||||
@ -156,18 +156,11 @@ export default function JobForm ({ item, sub }) {
|
|||||||
<PromoteJob item={item} sub={sub} />
|
<PromoteJob item={item} sub={sub} />
|
||||||
{item && <StatusControl item={item} />}
|
{item && <StatusControl item={item} />}
|
||||||
<div className='d-flex align-items-center justify-content-end mt-3'>
|
<div className='d-flex align-items-center justify-content-end mt-3'>
|
||||||
{item
|
<CancelButton />
|
||||||
? (
|
<FeeButton
|
||||||
<div className='d-flex'>
|
text={item ? 'save' : 'post'}
|
||||||
<CancelButton />
|
variant='secondary'
|
||||||
<SubmitButton variant='secondary'>save</SubmitButton>
|
/>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<ActionTooltip overlayText='1000 sats'>
|
|
||||||
<SubmitButton cost={1000} variant='secondary'>post <small> 1000 sats</small></SubmitButton>
|
|
||||||
</ActionTooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
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 { useRouter } from 'next/router'
|
||||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||||
import Countdown from './countdown'
|
import Countdown from './countdown'
|
||||||
@ -7,7 +7,7 @@ import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
|||||||
import { ITEM_FIELDS } from '../fragments/items'
|
import { ITEM_FIELDS } from '../fragments/items'
|
||||||
import Item from './item'
|
import Item from './item'
|
||||||
import AccordianItem from './accordian-item'
|
import AccordianItem from './accordian-item'
|
||||||
import FeeButton, { EditFeeButton } from './fee-button'
|
import FeeButton from './fee-button'
|
||||||
import Delete from './delete'
|
import Delete from './delete'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import { linkSchema } from '../lib/validate'
|
import { linkSchema } from '../lib/validate'
|
||||||
@ -194,33 +194,24 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||||||
/>
|
/>
|
||||||
</AdvPostForm>
|
</AdvPostForm>
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
{item
|
<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}`)}>
|
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
|
||||||
<Button variant='grey-medium'>delete</Button>
|
<Button variant='grey-medium'>delete</Button>
|
||||||
</Delete>
|
</Delete>)
|
||||||
<div className='d-flex'>
|
: dupesLoading &&
|
||||||
<CancelButton />
|
<div className='d-flex justify-content-center'>
|
||||||
<EditFeeButton
|
<Moon className='spin fill-grey' />
|
||||||
paidSats={item.meSats}
|
<div className='ms-2 text-muted' style={{ fontWeight: '600' }}>searching for dupes</div>
|
||||||
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
|
</div>}
|
||||||
/>
|
<div className='d-flex align-items-center ms-auto'>
|
||||||
</div>
|
<CancelButton />
|
||||||
</div>)
|
<FeeButton
|
||||||
: (
|
text={item ? 'save' : 'post'} disabled={postDisabled} variant='secondary'
|
||||||
<div className='d-flex align-items-center'>
|
/>
|
||||||
<FeeButton
|
</div>
|
||||||
baseFee={1} parentId={null} text='post' disabled={postDisabled}
|
</div>
|
||||||
ChildButton={SubmitButton} variant='secondary'
|
|
||||||
/>
|
|
||||||
{dupesLoading &&
|
|
||||||
<div className='d-flex ms-3 justify-content-center'>
|
|
||||||
<Moon className='spin fill-grey' />
|
|
||||||
<div className='ms-2 text-muted' style={{ fontWeight: '600' }}>searching for dupes</div>
|
|
||||||
</div>}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{!item &&
|
{!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 { useRouter } from 'next/router'
|
||||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||||
import Countdown from './countdown'
|
import Countdown from './countdown'
|
||||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
||||||
import { MAX_POLL_CHOICE_LENGTH, MAX_POLL_NUM_CHOICES, MAX_TITLE_LENGTH } from '../lib/constants'
|
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 Delete from './delete'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import { pollSchema } from '../lib/validate'
|
import { pollSchema } from '../lib/validate'
|
||||||
@ -99,24 +99,20 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||||||
/>
|
/>
|
||||||
<AdvPostForm edit={!!item} />
|
<AdvPostForm edit={!!item} />
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
{item
|
<div className='mt-3'>
|
||||||
? (
|
<div className='d-flex justify-content-between'>
|
||||||
<div className='d-flex justify-content-between'>
|
{item &&
|
||||||
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
|
<Delete itemId={item.id} onDelete={() => router.push(`/items/${item.id}`)}>
|
||||||
<Button variant='grey-medium'>delete</Button>
|
<Button variant='grey-medium'>delete</Button>
|
||||||
</Delete>
|
</Delete>}
|
||||||
<div className='d-flex'>
|
<div className='d-flex align-items-center ms-auto'>
|
||||||
<CancelButton />
|
<CancelButton />
|
||||||
<EditFeeButton
|
<FeeButton
|
||||||
paidSats={item.meSats}
|
text={item ? 'save' : 'post'} variant='secondary'
|
||||||
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>)
|
</div>
|
||||||
: <FeeButton
|
|
||||||
baseFee={1} parentId={null} text='post'
|
|
||||||
ChildButton={SubmitButton} variant='secondary'
|
|
||||||
/>}
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
@ -12,6 +12,7 @@ import { BountyForm } from './bounty-form'
|
|||||||
import SubSelect from './sub-select-form'
|
import SubSelect from './sub-select-form'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
import { useCallback, useState } from 'react'
|
import { useCallback, useState } from 'react'
|
||||||
|
import { FeeButtonProvider, postCommentBaseLineItems, postCommentUseRemoteLineItems } from './fee-button'
|
||||||
|
|
||||||
function FreebieDialog () {
|
function FreebieDialog () {
|
||||||
return (
|
return (
|
||||||
@ -94,7 +95,14 @@ export function PostForm ({ type, sub, children }) {
|
|||||||
FormType = BountyForm
|
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 }) {
|
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 { gql, useMutation } from '@apollo/client'
|
||||||
import styles from './reply.module.css'
|
import styles from './reply.module.css'
|
||||||
import { COMMENTS } from '../fragments/comments'
|
import { COMMENTS } from '../fragments/comments'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { forwardRef, useCallback, useEffect, useState, useRef, useImperativeHandle } from 'react'
|
import { forwardRef, useCallback, useEffect, useState, useRef, useImperativeHandle } from 'react'
|
||||||
import Link from 'next/link'
|
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 { commentsViewedAfterComment } from '../lib/new-comments'
|
||||||
import { commentSchema } from '../lib/validate'
|
import { commentSchema } from '../lib/validate'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
@ -183,35 +183,43 @@ export default forwardRef(function Reply ({ item, onSuccess, replyOpen, children
|
|||||||
{/* HACK if we need more items, we should probably do a comment toolbar */}
|
{/* HACK if we need more items, we should probably do a comment toolbar */}
|
||||||
{children}
|
{children}
|
||||||
</div>)}
|
</div>)}
|
||||||
<div className={styles.reply} style={{ display: reply ? 'block' : 'none' }}>
|
{reply &&
|
||||||
<Form
|
<div className={styles.reply}>
|
||||||
initial={{
|
<FeeButtonProvider
|
||||||
text: ''
|
baseLineItems={postCommentBaseLineItems({ baseCost: 1, comment: true, me: !!me })}
|
||||||
}}
|
useRemoteLineItems={postCommentUseRemoteLineItems({ parentId: item.id, me: !!me })}
|
||||||
schema={commentSchema}
|
>
|
||||||
invoiceable
|
<Form
|
||||||
onSubmit={onSubmit}
|
initial={{
|
||||||
storageKeyPrefix={`reply-${parentId}`}
|
text: ''
|
||||||
innerRef={formInnerRef}
|
}}
|
||||||
>
|
schema={commentSchema}
|
||||||
<MarkdownInput
|
invoiceable
|
||||||
name='text'
|
onSubmit={onSubmit}
|
||||||
minRows={6}
|
storageKeyPrefix={`reply-${parentId}`}
|
||||||
autoFocus={!replyOpen}
|
innerRef={formInnerRef}
|
||||||
required
|
>
|
||||||
placeholder={placeholder}
|
<MarkdownInput
|
||||||
hint={me?.sats < 1 && <FreebieDialog />}
|
name='text'
|
||||||
innerRef={replyInput}
|
minRows={6}
|
||||||
/>
|
autoFocus={!replyOpen}
|
||||||
{reply &&
|
required
|
||||||
<div className='mt-1'>
|
placeholder={placeholder}
|
||||||
<FeeButton
|
hint={me?.sats < 1 && <FreebieDialog />}
|
||||||
baseFee={1} parentId={parentId} text='reply'
|
innerRef={replyInput}
|
||||||
ChildButton={SubmitButton} variant='secondary' alwaysShow
|
|
||||||
/>
|
/>
|
||||||
</div>}
|
<div className='d-flex mt-1'>
|
||||||
</Form>
|
<div className='ms-auto'>
|
||||||
</div>
|
<FeeButton
|
||||||
|
text='reply'
|
||||||
|
variant='secondary'
|
||||||
|
alwaysShow
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</FeeButtonProvider>
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -29,11 +29,18 @@ function getClient (uri) {
|
|||||||
return new ApolloClient({
|
return new ApolloClient({
|
||||||
link: new HttpLink({ uri }),
|
link: new HttpLink({ uri }),
|
||||||
ssrMode: SSR,
|
ssrMode: SSR,
|
||||||
|
connectToDevTools: process.env.NODE_ENV !== 'production',
|
||||||
cache: new InMemoryCache({
|
cache: new InMemoryCache({
|
||||||
freezeResults: true,
|
freezeResults: true,
|
||||||
typePolicies: {
|
typePolicies: {
|
||||||
Query: {
|
Query: {
|
||||||
fields: {
|
fields: {
|
||||||
|
sub: {
|
||||||
|
keyArgs: ['name'],
|
||||||
|
merge (existing, incoming) {
|
||||||
|
return incoming
|
||||||
|
}
|
||||||
|
},
|
||||||
topUsers: {
|
topUsers: {
|
||||||
keyArgs: ['when', 'by', 'from', 'to', 'limit'],
|
keyArgs: ['when', 'by', 'from', 'to', 'limit'],
|
||||||
merge (existing, incoming) {
|
merge (existing, incoming) {
|
||||||
|
@ -5,12 +5,12 @@ import Button from 'react-bootstrap/Button'
|
|||||||
import styles from '../../styles/user.module.css'
|
import styles from '../../styles/user.module.css'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import ItemFull from '../../components/item-full'
|
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 { useMe } from '../../components/me'
|
||||||
import { USER_FULL } from '../../fragments/users'
|
import { USER_FULL } from '../../fragments/users'
|
||||||
import { ITEM_FIELDS } from '../../fragments/items'
|
import { ITEM_FIELDS } from '../../fragments/items'
|
||||||
import { getGetServerSideProps } from '../../api/ssrApollo'
|
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 { bioSchema } from '../../lib/validate'
|
||||||
import CancelButton from '../../components/cancel-button'
|
import CancelButton from '../../components/cancel-button'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
@ -69,15 +69,9 @@ export function BioForm ({ handleDone, bio }) {
|
|||||||
/>
|
/>
|
||||||
<div className='d-flex mt-3 justify-content-end'>
|
<div className='d-flex mt-3 justify-content-end'>
|
||||||
<CancelButton onClick={handleDone} />
|
<CancelButton onClick={handleDone} />
|
||||||
{bio?.text
|
<FeeButtonProvider>
|
||||||
? <EditFeeButton
|
<FeeButton text='save' variant='secondary' />
|
||||||
paidSats={bio?.meSats}
|
</FeeButtonProvider>
|
||||||
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
|
|
||||||
/>
|
|
||||||
: <FeeButton
|
|
||||||
baseFee={1} parentId={null} text='create'
|
|
||||||
ChildButton={SubmitButton} variant='secondary'
|
|
||||||
/>}
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@ import { useState } from 'react'
|
|||||||
import { useQuery } from '@apollo/client'
|
import { useQuery } from '@apollo/client'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import PageLoading from '../../../components/page-loading'
|
import PageLoading from '../../../components/page-loading'
|
||||||
|
import { FeeButtonProvider } from '../../../components/fee-button'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({
|
export const getServerSideProps = getGetServerSideProps({
|
||||||
query: ITEM,
|
query: ITEM,
|
||||||
@ -39,11 +40,23 @@ export default function PostEdit ({ ssrData }) {
|
|||||||
FormType = BountyForm
|
FormType = BountyForm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingBoostLineItem = item.boost
|
||||||
|
? {
|
||||||
|
existingBoost: {
|
||||||
|
label: 'old boost',
|
||||||
|
term: `- ${item.boost}`,
|
||||||
|
modifier: cost => cost - item.boost
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenterLayout sub={sub}>
|
<CenterLayout sub={sub}>
|
||||||
<FormType item={item} editThreshold={editThreshold}>
|
<FeeButtonProvider baseLineItems={existingBoostLineItem}>
|
||||||
{!item.isJob && <SubSelect label='sub' item={item} setSub={setSub} sub={sub} />}
|
<FormType item={item} editThreshold={editThreshold}>
|
||||||
</FormType>
|
{!item.isJob && <SubSelect label='sub' item={item} setSub={setSub} sub={sub} />}
|
||||||
|
</FormType>
|
||||||
|
</FeeButtonProvider>
|
||||||
</CenterLayout>
|
</CenterLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ export function DonateButton () {
|
|||||||
<Button onClick={() => showModal(onClose => (
|
<Button onClick={() => showModal(onClose => (
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
amount: 1000
|
amount: 10000
|
||||||
}}
|
}}
|
||||||
schema={amountSchema}
|
schema={amountSchema}
|
||||||
invoiceable
|
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…
x
Reference in New Issue
Block a user