diff --git a/.gitignore b/.gitignore index 4adad4b6b..cfb07a158 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ .DS_Store *.pem /*.sql +/*.patch # debug npm-debug.log* diff --git a/api/resolvers/item.js b/api/resolvers/item.js index dce6d2f07..839936c32 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -9,14 +9,14 @@ import { ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY, ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE, ANON_ITEM_SPAM_INTERVAL, POLL_COST, - ITEM_ALLOW_EDITS, GLOBAL_SEED + ITEM_ALLOW_EDITS, GLOBAL_SEED, UPPER_CHARS_TITLE_FEE_MULT } from '../../lib/constants' import { msatsToSats } from '../../lib/format' import { parse } from 'tldts' import uu from 'url-unshort' import { advSchema, amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate' import { sendUserNotification } from '../webPush' -import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '../../lib/item' +import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand, titleExceedsFreeUppercase } from '../../lib/item' import { notifyItemParents, notifyUserSubscribers, notifyZapped } from '../../lib/push-notifications' import { datePivot, whenRange } from '../../lib/time' import { imageFeesInfo, uploadIdsFromText } from './image' @@ -1092,10 +1092,35 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it const uploadIds = uploadIdsFromText(item.text, { models }) const { fees: imgFees } = await imageFeesInfo(uploadIds, { models, me }) + const hasPaidUpperTitleFee = old.upperTitleFeePaid + const titleUpperMult = !hasPaidUpperTitleFee && titleExceedsFreeUppercase(item) ? UPPER_CHARS_TITLE_FEE_MULT : 1 + let additionalFeeMsats = 0 + if (titleUpperMult > 1) { + const { _sum: { msats: paidMsats } } = await models.itemAct.aggregate({ + _sum: { + msats: true + }, + where: { + itemId: Number(item.id), + userId: me.id, + act: 'FEE' + } + }) + const { baseCost } = await models.sub.findUnique({ where: { name: item.subName || item.root?.subName } }) + // If the original post was a freebie, that doesn't mean this edit should be free + if (Number(paidMsats) === 0) { + additionalFeeMsats = titleUpperMult * baseCost * 1000 + item.freebie = false + } else { + additionalFeeMsats = (titleUpperMult - 1) * baseCost * 1000 + } + item.upperTitleFeePaid = true + } + item = await serializeInvoicable( - models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB, $4::INTEGER[]) AS "Item"`, - JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options), uploadIds), - { models, lnd, hash, hmac, me, enforceFee: imgFees } + models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB, $4::INTEGER[], $5::BIGINT) AS "Item"`, + JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options), uploadIds, additionalFeeMsats), + { models, lnd, hash, hmac, me, enforceFee: imgFees + additionalFeeMsats } ) await createMentions(item, models) @@ -1128,11 +1153,14 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo const uploadIds = uploadIdsFromText(item.text, { models }) const { fees: imgFees } = await imageFeesInfo(uploadIds, { models, me }) - const enforceFee = (me ? undefined : (item.parentId ? ANON_COMMENT_FEE : (ANON_POST_FEE + (item.boost || 0)))) + imgFees + const titleUpperMult = titleExceedsFreeUppercase(item) ? UPPER_CHARS_TITLE_FEE_MULT : 1 + item.upperTitleFeePaid = titleUpperMult > 1 + + const enforceFee = (me ? undefined : (item.parentId ? ANON_COMMENT_FEE : (ANON_POST_FEE * titleUpperMult + (item.boost || 0)))) + imgFees item = await serializeInvoicable( models.$queryRawUnsafe( - `${SELECT} FROM create_item($1::JSONB, $2::JSONB, $3::JSONB, '${spamInterval}'::INTERVAL, $4::INTEGER[]) AS "Item"`, - JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options), uploadIds), + `${SELECT} FROM create_item($1::JSONB, $2::JSONB, $3::JSONB, '${spamInterval}'::INTERVAL, $4::INTEGER[], $5::INTEGER) AS "Item"`, + JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options), uploadIds, titleUpperMult), { models, lnd, hash, hmac, me, enforceFee } ) @@ -1184,7 +1212,8 @@ export const SELECT = "Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost", "Item".boost, "Item".msats, "Item".ncomments, "Item"."commentMsats", "Item"."lastCommentAt", "Item"."weightedVotes", "Item"."weightedDownVotes", "Item".freebie, "Item".bio, "Item"."otsHash", "Item"."bountyPaidTo", - ltree2text("Item"."path") AS "path", "Item"."weightedComments", "Item"."imgproxyUrls"` + ltree2text("Item"."path") AS "path", "Item"."weightedComments", "Item"."imgproxyUrls", + "Item"."upperTitleFeePaid"` function topOrderByWeightedSats (me, models) { return `ORDER BY ${orderByNumerator(models)} DESC NULLS LAST, "Item".id DESC` diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index e915c01e3..98aa4d9b9 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -117,6 +117,7 @@ export default gql` parentOtsHash: String forwards: [ItemForward] imgproxyUrls: JSONObject + upperTitleFeePaid: Boolean } input ItemForwardInput { diff --git a/components/adv-post-form.js b/components/adv-post-form.js index 849dc1ac4..ec4523dcf 100644 --- a/components/adv-post-form.js +++ b/components/adv-post-form.js @@ -55,9 +55,10 @@ export default function AdvPostForm ({ children }) { name='boost' onChange={(_, e) => merge({ boost: { - term: `+ ${e.target.value}`, + term: `+ ${numWithUnits(e.target.value, { abbreviate: false, format: true })}`, label: 'boost', - modifier: cost => cost + Number(e.target.value) + modifier: cost => cost + Number(e.target.value), + omit: !Number(e.target.value) } })} hint={ranks posts higher temporarily based on the amount} diff --git a/components/bounty-form.js b/components/bounty-form.js index 362d2f608..01278857e 100644 --- a/components/bounty-form.js +++ b/components/bounty-form.js @@ -3,6 +3,7 @@ import { useRouter } from 'next/router' import { gql, useApolloClient, useMutation } from '@apollo/client' import Countdown from './countdown' import AdvPostForm, { AdvPostInitial } from './adv-post-form' +import { useFeeButton, uppercaseTitleFeeHandler } from './fee-button' import InputGroup from 'react-bootstrap/InputGroup' import { bountySchema } from '../lib/validate' import { SubSelectInitial } from './sub-select-form' @@ -84,6 +85,7 @@ export function BountyForm ({ toastDeleteScheduled(toaster, data, !!item, values.text) }, [upsertBounty, router] ) + const feeButton = useFeeButton() return (