8f590425dc
* Add icon to add images * Open file explorer to select image * Upload images to S3 on selection * Show uploaded images below text input * Link and remove image * Fetch unsubmitted images from database * Mark S3 images as submitted in imgproxy job * Add margin-top * Mark images as submitted on client after successful mutation * Also delete objects in S3 * Allow items to have multiple uploads linked * Overwrite old avatar * Add fees for presigned URLs * Use Github style upload * removed upfront fees * removed images provider since we no longer need to keep track of unsubmitted images on the client * removed User.images resolver * removed deleteImage mutation * use Github style upload where it shows ![Uploading <filename>...]() first and then replaces that with ![<filename>](<url>) after successful upload * Add Upload.paid boolean column One item can have multiple images linked to it, but an image can also be used in multiple items (many-to-many relation). Since we don't really care to which item an image is linked and vice versa, we just use a boolean column to mark if an image was already paid for. This makes fee calculation easier since no JOINs are required. * Add image fees during item creation/update * we calculate image fees during item creation and update now * function imageFees returns queries which deduct fees from user and mark images as paid + fees * queries need to be run inside same transaction as item creation/update * Allow anons to get presigned URLs * Add comments regarding avatar upload * Use megabytes in error message * Remove unnecessary avatar check during image fees calculation * Show image fees in frontend * Also update image fees on blur This makes sure that the images fees reflect the current state. For example, if an image was removed. We could also add debounced requests. * Show amount of unpaid images in receipt * Fix fees in sats deducted from msats * Fix algebraic order of fees Spam fees must come immediately after the base fee since it multiplies the base fee. * Fix image fees in edit receipt * Fix stale fees shown If we pay for an image and then want to edit the comment, the cache might return stale date; suggesting we didn't pay for the existing image yet. * Add 0 base fee in edit receipt * Remove 's' from 'image fees' in receipts * Remove unnecessary async * Remove 'Uploading <name>...' from text input on error * Support upload of multiple files at once * Add schedule to delete unused images * Fix image fee display in receipts * Use Drag and Drop API for image upload * Remove dragOver style on drop * Increase max upload size to 10MB to allow HQ camera pictures * Fix free upload quota * Fix stale image fees served * Fix bad image fee return statements * Fix multiplication with feesPerImage * Fix NULL returned for size24h, sizeNow * Remove unnecessary text field in query * refactor: Unify <ImageUpload> and <Upload> component * Add avatar cache busting using random query param * Calculate image fee info in postgres function * we now calculate image fee info in a postgres function which is much cleaner * we use this function inside `create_item` and `update_item`: image fees are now deducted in the same transaction as creating/updating the item! * reversed changes in `serializeInvoiceable` * Fix line break in receipt * Update upload limits * Add comment about `e.target.value = null` * Use debounce instead of onBlur to update image fees info * Fix invoice amount * Refactor avatar upload control flow * Update image fees in onChange * Fix rescheduling of other jobs * also update schedule from every minute to every hour * Add image fees in calling context * keep item ids on uploads * Fix incompatible onSubmit signature * Revert "keep item ids on uploads" This reverts commit 4688962abcd54fdc5850109372a7ad054cf9b2e4. * many2many item uploads * pretty subdomain for images * handle upload conditions for profile images and job logos --------- Co-authored-by: ekzyis <ek@ekzyis.com> Co-authored-by: ekzyis <ek@stacker.news>
160 lines
6.2 KiB
JavaScript
160 lines
6.2 KiB
JavaScript
import { useEffect } 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 { numWithUnits } from '../lib/format'
|
|
import { useMe } from './me'
|
|
import AnonIcon from '../svgs/spy-fill.svg'
|
|
import { useShowModal } from './modal'
|
|
import Link from 'next/link'
|
|
|
|
function Receipt ({ cost, repetition, imageFeesInfo, baseFee, parentId, boost }) {
|
|
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>}
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<td className='fw-bold'>{numWithUnits(cost, { abbreviate: false })}</td>
|
|
<td align='right' className='font-weight-light'>total fee</td>
|
|
</tr>
|
|
</tfoot>
|
|
</Table>
|
|
)
|
|
}
|
|
|
|
function AnonInfo () {
|
|
const showModal = useShowModal()
|
|
|
|
return (
|
|
<AnonIcon
|
|
className='fill-muted ms-2 theme' height={22} width={22}
|
|
onClick={
|
|
(e) =>
|
|
showModal(onClose =>
|
|
<div><div className='fw-bold text-center'>You are posting without an account</div>
|
|
<ol className='my-3'>
|
|
<li>You'll pay by invoice</li>
|
|
<li>Your content will be content-joined (get it?!) under the <Link href='/anon' target='_blank'>@anon</Link> account</li>
|
|
<li>Any sats your content earns will go toward <Link href='/rewards' target='_blank'>rewards</Link></li>
|
|
<li>We won't be able to notify you when you receive replies</li>
|
|
</ol>
|
|
<small className='text-center fst-italic text-muted'>btw if you don't need to be anonymous, posting is cheaper with an account</small>
|
|
</div>)
|
|
}
|
|
/>
|
|
)
|
|
}
|
|
|
|
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>
|
|
)
|
|
}
|