stacker.news/components/fee-button.js

180 lines
5.8 KiB
JavaScript
Raw Normal View History

2023-11-11 00:18:10 +00:00
import { useEffect, useContext, createContext, useState, useCallback, useMemo } from 'react'
2023-07-24 18:35:05 +00:00
import Table from 'react-bootstrap/Table'
2022-08-10 15:06:31 +00:00
import ActionTooltip from './action-tooltip'
import Info from './info'
import styles from './fee-button.module.css'
import { gql, useQuery } from '@apollo/client'
2023-11-19 20:16:35 +00:00
import { FREEBIE_BASE_COST_THRESHOLD, 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'
2023-11-11 00:18:10 +00:00
import { SubmitButton } from './form'
2022-08-10 15:06:31 +00:00
2023-11-11 00:18:10 +00:00
const FeeButtonContext = createContext()
2022-08-10 15:06:31 +00:00
2023-11-11 00:18:10 +00:00
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
}
}
2023-11-11 00:18:10 +00:00
return {
baseCost: {
term: baseCost,
label: `${comment ? 'comment' : 'post'} cost`,
modifier: (cost) => cost + baseCost
},
...anonCharge
}
}
2023-11-11 00:18:10 +00:00
export function postCommentUseRemoteLineItems ({ parentId, me } = {}) {
if (!me) return () => {}
2022-08-10 15:06:31 +00:00
const query = parentId
? gql`{ itemRepetition(parentId: "${parentId}") }`
: gql`{ itemRepetition }`
2023-11-11 00:18:10 +00:00
return function useRemoteLineItems () {
const [line, setLine] = useState({})
const { data } = useQuery(query, SSR ? {} : { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
2022-08-10 15:06:31 +00:00
2023-11-11 00:18:10 +00:00
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
}
}
2023-11-11 00:18:10 +00:00
export function FeeButtonProvider ({ baseLineItems = {}, useRemoteLineItems = () => null, children }) {
const [lineItems, setLineItems] = useState({})
const [disabled, setDisabled] = useState(false)
2023-11-11 00:18:10 +00:00
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),
disabled,
setDisabled
2023-11-11 00:18:10 +00:00
}
}, [baseLineItems, lineItems, remoteLineItems, mergeLineItems, disabled, setDisabled])
2023-11-11 00:18:10 +00:00
return (
<FeeButtonContext.Provider value={value}>
{children}
</FeeButtonContext.Provider>
)
}
export function useFeeButton () {
return useContext(FeeButtonContext)
}
2023-11-19 20:16:35 +00:00
function FreebieDialog () {
return (
<>
<div className='fw-bold'>you don't have enough sats, so this one is on us</div>
<ul className='mt-2'>
<li>Free items have limited visibility until other stackers zap them.</li>
<li>To get fully visibile right away, fund your account with a few sats or earn some on Stacker News.</li>
</ul>
</>
)
}
2023-11-11 00:18:10 +00:00
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
const me = useMe()
const { lines, total, disabled: ctxDisabled } = useFeeButton()
2023-11-19 20:16:35 +00:00
// freebies: there's only a base cost, it's less than 10, and we have less than 10 sats
const free = total === lines.baseCost?.modifier(0) &&
total <= FREEBIE_BASE_COST_THRESHOLD &&
me?.privates?.sats < FREEBIE_BASE_COST_THRESHOLD
2023-11-19 20:16:35 +00:00
const feeText = free
? 'free'
: total > 1
? numWithUnits(total, { abbreviate: false, format: true })
: undefined
Image uploads (#576) * 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>
2023-11-06 20:53:33 +00:00
2022-08-10 15:06:31 +00:00
return (
<div className={styles.feeButton}>
2023-11-11 00:18:10 +00:00
<ActionTooltip overlayText={numWithUnits(total, { abbreviate: false })}>
2023-11-19 20:16:35 +00:00
<ChildButton variant={variant} disabled={disabled || ctxDisabled}>{text}{feeText && <small> {feeText}</small>}</ChildButton>
2022-08-10 15:06:31 +00:00
</ActionTooltip>
{!me && <AnonInfo />}
2023-11-19 20:16:35 +00:00
{(free && <Info><FreebieDialog /></Info>) ||
(total > 1 && <Info><Receipt lines={lines} total={total} /></Info>)}
2022-08-10 15:06:31 +00:00
</div>
)
}
2022-08-18 18:15:24 +00:00
2023-11-11 00:18:10 +00:00
function Receipt ({ lines, total }) {
2022-08-18 18:15:24 +00:00
return (
<Table className={styles.receipt} borderless size='sm'>
<tbody>
2023-11-11 00:18:10 +00:00
{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>))}
2022-08-18 18:15:24 +00:00
</tbody>
<tfoot>
<tr>
2023-11-11 00:18:10 +00:00
<td className='fw-bold'>{numWithUnits(total, { abbreviate: false, format: true })}</td>
2022-08-18 18:15:24 +00:00
<td align='right' className='font-weight-light'>total fee</td>
</tr>
</tfoot>
</Table>
)
}
2023-11-11 00:18:10 +00:00
function AnonInfo () {
const showModal = useShowModal()
Image uploads (#576) * 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>
2023-11-06 20:53:33 +00:00
2022-08-18 18:15:24 +00:00
return (
2023-11-11 00:18:10 +00:00
<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>)
}
/>
2022-08-18 18:15:24 +00:00
)
}