Show image fees in frontend
This commit is contained in:
parent
1af6a5c98d
commit
221dd5bb1d
85
api/resolvers/image.js
Normal file
85
api/resolvers/image.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { ANON_USER_ID, AWS_S3_URL_REGEXP } from '../../lib/constants'
|
||||||
|
import { datePivot } from '../../lib/time'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Query: {
|
||||||
|
imageFees: async (parent, { s3Keys }, { models, me }) => {
|
||||||
|
const [, fees] = await imageFees(s3Keys, { models, me })
|
||||||
|
return fees
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function imageFeesFromText (text, { models, me }) {
|
||||||
|
// no text means no image fees
|
||||||
|
if (!text) return [itemId => [], 0]
|
||||||
|
|
||||||
|
// parse all s3 keys (= image ids) from text
|
||||||
|
const textS3Keys = [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1]))
|
||||||
|
if (!textS3Keys.length) return [itemId => [], 0]
|
||||||
|
|
||||||
|
return imageFees(textS3Keys, { models, me })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function imageFees (s3Keys, { models, me }) {
|
||||||
|
// To apply image fees, we return queries which need to be run, preferably in the same transaction as creating or updating an item.
|
||||||
|
function queries (userId, imgIds, imgFees) {
|
||||||
|
return itemId => {
|
||||||
|
return [
|
||||||
|
// pay fees
|
||||||
|
models.$queryRawUnsafe('SELECT * FROM user_fee($1::INTEGER, $2::INTEGER, $3::BIGINT)', userId, itemId, imgFees),
|
||||||
|
// mark images as paid
|
||||||
|
models.upload.updateMany({ where: { id: { in: imgIds } }, data: { paid: true } })
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we want to ignore image ids for which someone already paid during fee calculation
|
||||||
|
// to make sure that every image is only paid once
|
||||||
|
const unpaidS3Keys = (await models.upload.findMany({ select: { id: true }, where: { id: { in: s3Keys }, paid: false } })).map(({ id }) => id)
|
||||||
|
const unpaid = unpaidS3Keys.length
|
||||||
|
|
||||||
|
if (!unpaid) return [itemId => [], 0]
|
||||||
|
|
||||||
|
if (!me) {
|
||||||
|
// anons pay for every new image 100 sats
|
||||||
|
const fees = unpaid * 100
|
||||||
|
return [queries(ANON_USER_ID, unpaidS3Keys, fees), fees]
|
||||||
|
}
|
||||||
|
|
||||||
|
// check how much stacker uploaded in last 24 hours
|
||||||
|
const { _sum: { size: size24h } } = await models.upload.aggregate({
|
||||||
|
_sum: { size: true },
|
||||||
|
where: {
|
||||||
|
userId: me.id,
|
||||||
|
createdAt: { gt: datePivot(new Date(), { days: -1 }) },
|
||||||
|
paid: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// check how much stacker uploaded now in size
|
||||||
|
const { _sum: { size: sizeNow } } = await models.upload.aggregate({
|
||||||
|
_count: { id: true },
|
||||||
|
_sum: { size: true },
|
||||||
|
where: { id: { in: unpaidS3Keys } }
|
||||||
|
})
|
||||||
|
|
||||||
|
// total size that we consider to calculate fees includes size of images within last 24 hours and size of incoming images
|
||||||
|
const size = size24h + sizeNow
|
||||||
|
const MB = 1024 * 1024 // factor for bytes -> megabytes
|
||||||
|
|
||||||
|
// 10 MB per 24 hours are free. fee is also 0 if there are no incoming images (obviously)
|
||||||
|
let fees
|
||||||
|
if (!sizeNow || size <= 1 * MB) {
|
||||||
|
fees = 0
|
||||||
|
} else if (size <= 25 * MB) {
|
||||||
|
fees = 10 * unpaid
|
||||||
|
} else if (size <= 50 * MB) {
|
||||||
|
fees = 100 * unpaid
|
||||||
|
} else if (size <= 100 * MB) {
|
||||||
|
fees = 1000 * unpaid
|
||||||
|
} else {
|
||||||
|
fees = 10000 * unpaid
|
||||||
|
}
|
||||||
|
return [queries(me.id, unpaidS3Keys, fees), fees]
|
||||||
|
}
|
@ -15,6 +15,7 @@ import price from './price'
|
|||||||
import { GraphQLJSONObject as JSONObject } from 'graphql-type-json'
|
import { GraphQLJSONObject as JSONObject } from 'graphql-type-json'
|
||||||
import admin from './admin'
|
import admin from './admin'
|
||||||
import blockHeight from './blockHeight'
|
import blockHeight from './blockHeight'
|
||||||
|
import image from './image'
|
||||||
import { GraphQLScalarType, Kind } from 'graphql'
|
import { GraphQLScalarType, Kind } from 'graphql'
|
||||||
|
|
||||||
const date = new GraphQLScalarType({
|
const date = new GraphQLScalarType({
|
||||||
@ -45,4 +46,4 @@ const date = new GraphQLScalarType({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
|
export default [user, item, message, wallet, lnurl, notifications, invite, sub,
|
||||||
upload, search, growth, rewards, referrals, price, admin, blockHeight, { JSONObject }, { Date: date }]
|
upload, search, growth, rewards, referrals, price, admin, blockHeight, image, { JSONObject }, { Date: date }]
|
||||||
|
@ -19,6 +19,7 @@ import { sendUserNotification } from '../webPush'
|
|||||||
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '../../lib/item'
|
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '../../lib/item'
|
||||||
import { notifyItemParents, notifyUserSubscribers, notifyZapped } from '../../lib/push-notifications'
|
import { notifyItemParents, notifyUserSubscribers, notifyZapped } from '../../lib/push-notifications'
|
||||||
import { datePivot } from '../../lib/time'
|
import { datePivot } from '../../lib/time'
|
||||||
|
import { imageFeesFromText } from './image'
|
||||||
|
|
||||||
export async function commentFilterClause (me, models) {
|
export async function commentFilterClause (me, models) {
|
||||||
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
|
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
|
||||||
@ -1090,7 +1091,7 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
|
|||||||
item = { subName, userId: me.id, ...item }
|
item = { subName, userId: me.id, ...item }
|
||||||
const fwdUsers = await getForwardUsers(models, forward)
|
const fwdUsers = await getForwardUsers(models, forward)
|
||||||
|
|
||||||
const [imgQueriesFn, imgFees] = await imageFees(item.text, { models, me })
|
const [imgQueriesFn, imgFees] = await imageFeesFromText(item.text, { models, me })
|
||||||
item = await serializeInvoicable(
|
item = await serializeInvoicable(
|
||||||
[
|
[
|
||||||
models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB) AS "Item"`,
|
models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB) AS "Item"`,
|
||||||
@ -1127,7 +1128,7 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
|
|||||||
item.url = removeTracking(item.url)
|
item.url = removeTracking(item.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [imgQueriesFn, imgFees] = await imageFees(item.text, { models, me })
|
const [imgQueriesFn, imgFees] = await imageFeesFromText(item.text, { models, me })
|
||||||
const enforceFee = (me ? undefined : (item.parentId ? ANON_COMMENT_FEE : (ANON_POST_FEE + (item.boost || 0)))) + imgFees
|
const enforceFee = (me ? undefined : (item.parentId ? ANON_COMMENT_FEE : (ANON_POST_FEE + (item.boost || 0)))) + imgFees
|
||||||
item = await serializeInvoicable(
|
item = await serializeInvoicable(
|
||||||
models.$queryRawUnsafe(
|
models.$queryRawUnsafe(
|
||||||
@ -1150,77 +1151,6 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
const AWS_S3_URL_REGEXP = new RegExp(`https://${process.env.NEXT_PUBLIC_AWS_UPLOAD_BUCKET}.s3.amazonaws.com/([0-9]+)`, 'g')
|
|
||||||
async function imageFees (text, { models, me }) {
|
|
||||||
// To apply image fees, we return queries which need to be run, preferably in the same transaction as creating or updating an item.
|
|
||||||
function queries (userId, imgIds, imgFees) {
|
|
||||||
return itemId => {
|
|
||||||
return [
|
|
||||||
// pay fees
|
|
||||||
models.$queryRawUnsafe('SELECT * FROM user_fee($1::INTEGER, $2::INTEGER, $3::BIGINT)', userId, itemId, imgFees),
|
|
||||||
// mark images as paid
|
|
||||||
models.upload.updateMany({ where: { id: { in: imgIds } }, data: { paid: true } })
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// no text means no image fees
|
|
||||||
if (!text) return [itemId => [], 0]
|
|
||||||
|
|
||||||
// parse all s3 keys (= image ids) from text
|
|
||||||
const textS3Keys = [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1]))
|
|
||||||
if (!textS3Keys.length) return [itemId => [], 0]
|
|
||||||
|
|
||||||
// we want to ignore image ids in text for which someone already paid during fee calculation
|
|
||||||
// to make sure that every image is only paid once
|
|
||||||
const unpaidS3Keys = (await models.upload.findMany({ select: { id: true }, where: { id: { in: textS3Keys }, paid: false } })).map(({ id }) => id)
|
|
||||||
const unpaid = unpaidS3Keys.length
|
|
||||||
|
|
||||||
if (!unpaid) return [itemId => [], 0]
|
|
||||||
|
|
||||||
if (!me) {
|
|
||||||
// anons pay for every new image 100 sats
|
|
||||||
const fees = unpaid * 100
|
|
||||||
return [queries(ANON_USER_ID, unpaidS3Keys, fees), fees]
|
|
||||||
}
|
|
||||||
|
|
||||||
// check how much stacker uploaded in last 24 hours
|
|
||||||
const { _sum: { size: size24h } } = await models.upload.aggregate({
|
|
||||||
_sum: { size: true },
|
|
||||||
where: {
|
|
||||||
userId: me.id,
|
|
||||||
createdAt: { gt: datePivot(new Date(), { days: -1 }) },
|
|
||||||
paid: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// check how much stacker uploaded now in size
|
|
||||||
const { _sum: { size: sizeNow } } = await models.upload.aggregate({
|
|
||||||
_count: { id: true },
|
|
||||||
_sum: { size: true },
|
|
||||||
where: { id: { in: unpaidS3Keys } }
|
|
||||||
})
|
|
||||||
|
|
||||||
// total size that we consider to calculate fees includes size of images within last 24 hours and size of incoming images
|
|
||||||
const size = size24h + sizeNow
|
|
||||||
const MB = 1024 * 1024 // factor for bytes -> megabytes
|
|
||||||
|
|
||||||
// 10 MB per 24 hours are free. fee is also 0 if there are no incoming images (obviously)
|
|
||||||
let fees
|
|
||||||
if (!sizeNow || size <= 10 * MB) {
|
|
||||||
fees = 0
|
|
||||||
} else if (size <= 25 * MB) {
|
|
||||||
fees = 10 * unpaid
|
|
||||||
} else if (size <= 50 * MB) {
|
|
||||||
fees = 100 * unpaid
|
|
||||||
} else if (size <= 100 * MB) {
|
|
||||||
fees = 1000 * unpaid
|
|
||||||
} else {
|
|
||||||
fees = 10000 * unpaid
|
|
||||||
}
|
|
||||||
return [queries(me.id, unpaidS3Keys, fees), fees]
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearDeletionJobs = async (item, models) => {
|
const clearDeletionJobs = async (item, models) => {
|
||||||
await models.$queryRawUnsafe(`DELETE FROM pgboss.job WHERE name = 'deleteItem' AND data->>'id' = '${item.id}';`)
|
await models.$queryRawUnsafe(`DELETE FROM pgboss.job WHERE name = 'deleteItem' AND data->>'id' = '${item.id}';`)
|
||||||
}
|
}
|
||||||
|
7
api/typeDefs/image.js
Normal file
7
api/typeDefs/image.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { gql } from 'graphql-tag'
|
||||||
|
|
||||||
|
export default gql`
|
||||||
|
extend type Query {
|
||||||
|
imageFees(s3Keys: [Int]!): Int!
|
||||||
|
}
|
||||||
|
`
|
@ -16,6 +16,7 @@ import referrals from './referrals'
|
|||||||
import price from './price'
|
import price from './price'
|
||||||
import admin from './admin'
|
import admin from './admin'
|
||||||
import blockHeight from './blockHeight'
|
import blockHeight from './blockHeight'
|
||||||
|
import image from './image'
|
||||||
|
|
||||||
const common = gql`
|
const common = gql`
|
||||||
type Query {
|
type Query {
|
||||||
@ -35,4 +36,4 @@ const common = gql`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export default [common, user, item, itemForward, message, wallet, lnurl, notifications, invite,
|
export default [common, user, item, itemForward, message, wallet, lnurl, notifications, invite,
|
||||||
sub, upload, growth, rewards, referrals, price, admin, blockHeight]
|
sub, upload, growth, rewards, referrals, price, admin, blockHeight, image]
|
||||||
|
@ -12,7 +12,7 @@ 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'
|
||||||
|
|
||||||
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
function Receipt ({ cost, repetition, imageFees, baseFee, parentId, boost }) {
|
||||||
return (
|
return (
|
||||||
<Table className={styles.receipt} borderless size='sm'>
|
<Table className={styles.receipt} borderless size='sm'>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -20,10 +20,10 @@ function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
|||||||
<td>{numWithUnits(baseFee, { abbreviate: false })}</td>
|
<td>{numWithUnits(baseFee, { abbreviate: false })}</td>
|
||||||
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
||||||
</tr>
|
</tr>
|
||||||
{hasImgLink &&
|
{imageFees &&
|
||||||
<tr>
|
<tr>
|
||||||
<td>x 10</td>
|
<td>+ {numWithUnits(imageFees, { abbreviate: false })}</td>
|
||||||
<td align='right' className='font-weight-light'>image/link fee</td>
|
<td align='right' className='font-weight-light'>image fees</td>
|
||||||
</tr>}
|
</tr>}
|
||||||
{repetition > 0 &&
|
{repetition > 0 &&
|
||||||
<tr>
|
<tr>
|
||||||
@ -69,7 +69,7 @@ function AnonInfo () {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton, variant, text, alwaysShow, disabled }) {
|
export default function FeeButton ({ parentId, baseFee, ChildButton, variant, text, alwaysShow, disabled }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
baseFee = me ? baseFee : (parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
baseFee = me ? baseFee : (parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
||||||
const query = parentId
|
const query = parentId
|
||||||
@ -79,46 +79,39 @@ export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton,
|
|||||||
const repetition = me ? data?.itemRepetition || 0 : 0
|
const repetition = me ? data?.itemRepetition || 0 : 0
|
||||||
const formik = useFormikContext()
|
const formik = useFormikContext()
|
||||||
const boost = Number(formik?.values?.boost) || 0
|
const boost = Number(formik?.values?.boost) || 0
|
||||||
const cost = baseFee * (hasImgLink ? 10 : 1) * Math.pow(10, repetition) + Number(boost)
|
const cost = baseFee * Math.pow(10, repetition) + Number(boost)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik?.setFieldValue('cost', cost)
|
formik?.setFieldValue('cost', cost)
|
||||||
}, [formik?.getFieldProps('cost').value, cost])
|
}, [formik?.getFieldProps('cost').value, cost])
|
||||||
|
|
||||||
|
const imageFees = formik?.getFieldProps('imageFees').value || 0
|
||||||
|
const totalCost = cost + imageFees
|
||||||
|
|
||||||
const show = alwaysShow || !formik?.isSubmitting
|
const show = alwaysShow || !formik?.isSubmitting
|
||||||
return (
|
return (
|
||||||
<div className={styles.feeButton}>
|
<div className={styles.feeButton}>
|
||||||
<ActionTooltip overlayText={numWithUnits(cost, { abbreviate: false })}>
|
<ActionTooltip overlayText={numWithUnits(totalCost, { abbreviate: false })}>
|
||||||
<ChildButton variant={variant} disabled={disabled}>{text}{cost > 1 && show && <small> {numWithUnits(cost, { abbreviate: false })}</small>}</ChildButton>
|
<ChildButton variant={variant} disabled={disabled}>{text}{totalCost > 1 && show && <small> {numWithUnits(totalCost, { abbreviate: false })}</small>}</ChildButton>
|
||||||
</ActionTooltip>
|
</ActionTooltip>
|
||||||
{!me && <AnonInfo />}
|
{!me && <AnonInfo />}
|
||||||
{cost > baseFee && show &&
|
{totalCost > baseFee && show &&
|
||||||
<Info>
|
<Info>
|
||||||
<Receipt baseFee={baseFee} hasImgLink={hasImgLink} repetition={repetition} cost={cost} parentId={parentId} boost={boost} />
|
<Receipt baseFee={baseFee} imageFees={imageFees} repetition={repetition} cost={totalCost} parentId={parentId} boost={boost} />
|
||||||
</Info>}
|
</Info>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId }) {
|
function EditReceipt ({ cost, paidSats, imageFees, boost, parentId }) {
|
||||||
return (
|
return (
|
||||||
<Table className={styles.receipt} borderless size='sm'>
|
<Table className={styles.receipt} borderless size='sm'>
|
||||||
<tbody>
|
<tbody>
|
||||||
{addImgLink &&
|
{imageFees &&
|
||||||
<>
|
<tr>
|
||||||
<tr>
|
<td>+ {numWithUnits(imageFees, { abbreviate: false })}</td>
|
||||||
<td>{numWithUnits(paidSats, { abbreviate: false })}</td>
|
<td align='right' className='font-weight-light'>image fees</td>
|
||||||
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
</tr>}
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>x 10</td>
|
|
||||||
<td align='right' className='font-weight-light'>image/link fee</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>- {numWithUnits(paidSats, { abbreviate: false })}</td>
|
|
||||||
<td align='right' className='font-weight-light'>already paid</td>
|
|
||||||
</tr>
|
|
||||||
</>}
|
|
||||||
{boost > 0 &&
|
{boost > 0 &&
|
||||||
<tr>
|
<tr>
|
||||||
<td>+ {numWithUnits(boost, { abbreviate: false })}</td>
|
<td>+ {numWithUnits(boost, { abbreviate: false })}</td>
|
||||||
@ -135,25 +128,27 @@ function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditFeeButton ({ paidSats, hadImgLink, hasImgLink, ChildButton, variant, text, alwaysShow, parentId }) {
|
export function EditFeeButton ({ paidSats, ChildButton, variant, text, alwaysShow, parentId }) {
|
||||||
const formik = useFormikContext()
|
const formik = useFormikContext()
|
||||||
const boost = (formik?.values?.boost || 0) - (formik?.initialValues?.boost || 0)
|
const boost = (formik?.values?.boost || 0) - (formik?.initialValues?.boost || 0)
|
||||||
const addImgLink = hasImgLink && !hadImgLink
|
const cost = Number(boost)
|
||||||
const cost = (addImgLink ? paidSats * 9 : 0) + Number(boost)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formik?.setFieldValue('cost', cost)
|
formik?.setFieldValue('cost', cost)
|
||||||
}, [formik?.getFieldProps('cost').value, cost])
|
}, [formik?.getFieldProps('cost').value, cost])
|
||||||
|
|
||||||
|
const imageFees = formik?.getFieldProps('imageFees').value || 0
|
||||||
|
const totalCost = cost + imageFees
|
||||||
|
|
||||||
const show = alwaysShow || !formik?.isSubmitting
|
const show = alwaysShow || !formik?.isSubmitting
|
||||||
return (
|
return (
|
||||||
<div className='d-flex align-items-center'>
|
<div className='d-flex align-items-center'>
|
||||||
<ActionTooltip overlayText={numWithUnits(cost >= 0 ? cost : 0, { abbreviate: false })}>
|
<ActionTooltip overlayText={numWithUnits(totalCost >= 0 ? totalCost : 0, { abbreviate: false })}>
|
||||||
<ChildButton variant={variant}>{text}{cost > 0 && show && <small> {numWithUnits(cost, { abbreviate: false })}</small>}</ChildButton>
|
<ChildButton variant={variant}>{text}{totalCost > 0 && show && <small> {numWithUnits(totalCost, { abbreviate: false })}</small>}</ChildButton>
|
||||||
</ActionTooltip>
|
</ActionTooltip>
|
||||||
{cost > 0 && show &&
|
{totalCost > 0 && show &&
|
||||||
<Info>
|
<Info>
|
||||||
<EditReceipt paidSats={paidSats} addImgLink={addImgLink} cost={cost} parentId={parentId} boost={boost} />
|
<EditReceipt paidSats={paidSats} imageFees={imageFees} cost={totalCost} parentId={parentId} boost={boost} />
|
||||||
</Info>}
|
</Info>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -13,9 +13,8 @@ import AddImageIcon from '../svgs/image-add-line.svg'
|
|||||||
import styles from './form.module.css'
|
import styles from './form.module.css'
|
||||||
import Text from '../components/text'
|
import Text from '../components/text'
|
||||||
import AddIcon from '../svgs/add-fill.svg'
|
import AddIcon from '../svgs/add-fill.svg'
|
||||||
import { mdHas } from '../lib/md'
|
|
||||||
import CloseIcon from '../svgs/close-line.svg'
|
import CloseIcon from '../svgs/close-line.svg'
|
||||||
import { useLazyQuery } from '@apollo/client'
|
import { gql, useLazyQuery } from '@apollo/client'
|
||||||
import { TOP_USERS, USER_SEARCH } from '../fragments/users'
|
import { TOP_USERS, USER_SEARCH } from '../fragments/users'
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
import TextareaAutosize from 'react-textarea-autosize'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
@ -26,6 +25,7 @@ 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 { debounce } from './use-debounce-callback'
|
||||||
import { ImageUpload } from './image'
|
import { ImageUpload } from './image'
|
||||||
|
import { AWS_S3_URL_REGEXP } from '../lib/constants'
|
||||||
|
|
||||||
export function SubmitButton ({
|
export function SubmitButton ({
|
||||||
children, variant, value, onClick, disabled, cost, ...props
|
children, variant, value, onClick, disabled, cost, ...props
|
||||||
@ -95,12 +95,26 @@ export function InputSkeleton ({ label, hint }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_MENTION_INDICES = { start: -1, end: -1 }
|
const DEFAULT_MENTION_INDICES = { start: -1, end: -1 }
|
||||||
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setHasImgLink, onKeyDown, innerRef, ...props }) {
|
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKeyDown, innerRef, ...props }) {
|
||||||
const [tab, setTab] = useState('write')
|
const [tab, setTab] = useState('write')
|
||||||
const [, meta, helpers] = useField(props)
|
const [, meta, helpers] = useField(props)
|
||||||
const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 })
|
const [selectionRange, setSelectionRange] = useState({ start: 0, end: 0 })
|
||||||
innerRef = innerRef || useRef(null)
|
innerRef = innerRef || useRef(null)
|
||||||
const previousTab = useRef(tab)
|
const previousTab = useRef(tab)
|
||||||
|
const formik = useFormikContext()
|
||||||
|
const toaster = useToast()
|
||||||
|
const [updateImageFees] = useLazyQuery(gql`
|
||||||
|
query imageFees($s3Keys: [Int]!) {
|
||||||
|
imageFees(s3Keys: $s3Keys)
|
||||||
|
}`, {
|
||||||
|
onError: (err) => {
|
||||||
|
console.log(err)
|
||||||
|
toaster.danger(err.message || err.toString?.())
|
||||||
|
},
|
||||||
|
onCompleted: ({ imageFees }) => {
|
||||||
|
formik?.setFieldValue('imageFees', imageFees)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
props.as ||= TextareaAutosize
|
props.as ||= TextareaAutosize
|
||||||
props.rows ||= props.minRows || 6
|
props.rows ||= props.minRows || 6
|
||||||
@ -141,9 +155,6 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setH
|
|||||||
|
|
||||||
const onChangeInner = useCallback((formik, e) => {
|
const onChangeInner = useCallback((formik, e) => {
|
||||||
if (onChange) onChange(formik, e)
|
if (onChange) onChange(formik, e)
|
||||||
if (setHasImgLink) {
|
|
||||||
setHasImgLink(mdHas(e.target.value, ['link', 'image']))
|
|
||||||
}
|
|
||||||
// check for mention editing
|
// check for mention editing
|
||||||
const { value, selectionStart } = e.target
|
const { value, selectionStart } = e.target
|
||||||
let priorSpace = -1
|
let priorSpace = -1
|
||||||
@ -176,7 +187,7 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setH
|
|||||||
setMentionQuery(undefined)
|
setMentionQuery(undefined)
|
||||||
setMentionIndices(DEFAULT_MENTION_INDICES)
|
setMentionIndices(DEFAULT_MENTION_INDICES)
|
||||||
}
|
}
|
||||||
}, [onChange, setHasImgLink, setMentionQuery, setMentionIndices, setUserSuggestDropdownStyle])
|
}, [onChange, setMentionQuery, setMentionIndices, setUserSuggestDropdownStyle])
|
||||||
|
|
||||||
const onKeyDownInner = useCallback((userSuggestOnKeyDown) => {
|
const onKeyDownInner = useCallback((userSuggestOnKeyDown) => {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
@ -230,10 +241,12 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setH
|
|||||||
text += `![Uploading ${file.name}…]()`
|
text += `![Uploading ${file.name}…]()`
|
||||||
helpers.setValue(text)
|
helpers.setValue(text)
|
||||||
}}
|
}}
|
||||||
onSuccess={({ url, name }) => {
|
onSuccess={async ({ url, name }) => {
|
||||||
let text = innerRef.current.value
|
let text = innerRef.current.value
|
||||||
text = text.replace(`![Uploading ${name}…]()`, ``)
|
text = text.replace(`![Uploading ${name}…]()`, ``)
|
||||||
helpers.setValue(text)
|
helpers.setValue(text)
|
||||||
|
const s3Keys = [...text.matchAll(AWS_S3_URL_REGEXP)].map(m => Number(m[1]))
|
||||||
|
updateImageFees({ variables: { s3Keys } })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AddImageIcon width={18} height={18} />
|
<AddImageIcon width={18} height={18} />
|
||||||
|
@ -232,8 +232,9 @@ export const useInvoiceable = (onSubmit, options = defaultOptions) => {
|
|||||||
// this function will be called before the Form's onSubmit handler is called
|
// this function will be called before the Form's onSubmit handler is called
|
||||||
// and the form must include `cost` or `amount` as a value
|
// and the form must include `cost` or `amount` as a value
|
||||||
const onSubmitWrapper = useCallback(async (formValues, ...submitArgs) => {
|
const onSubmitWrapper = useCallback(async (formValues, ...submitArgs) => {
|
||||||
let { cost, amount } = formValues
|
let { cost, imageFees, amount } = formValues
|
||||||
cost ??= amount
|
cost ??= amount
|
||||||
|
if (imageFees) cost += imageFees
|
||||||
|
|
||||||
// action only allowed if logged in
|
// action only allowed if logged in
|
||||||
if (!me && options.requireSession) {
|
if (!me && options.requireSession) {
|
||||||
|
@ -8,6 +8,7 @@ export const BOOST_MULT = 5000
|
|||||||
export const BOOST_MIN = BOOST_MULT * 5
|
export const BOOST_MIN = BOOST_MULT * 5
|
||||||
export const UPLOAD_SIZE_MAX = 2 * 1024 * 1024
|
export const UPLOAD_SIZE_MAX = 2 * 1024 * 1024
|
||||||
export const IMAGE_PIXELS_MAX = 35000000
|
export const IMAGE_PIXELS_MAX = 35000000
|
||||||
|
export const AWS_S3_URL_REGEXP = new RegExp(`https://${process.env.NEXT_PUBLIC_AWS_UPLOAD_BUCKET}.s3.amazonaws.com/([0-9]+)`, 'g')
|
||||||
export const UPLOAD_TYPES_ALLOW = [
|
export const UPLOAD_TYPES_ALLOW = [
|
||||||
'image/gif',
|
'image/gif',
|
||||||
'image/heic',
|
'image/heic',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user