From d42be758b8da88ad519232376bd5df4190691a60 Mon Sep 17 00:00:00 2001 From: Soxasora <sora@soxa.dev> Date: Fri, 27 Dec 2024 16:26:12 +0100 Subject: [PATCH 1/7] feat: comment fee control --- api/paidAction/itemCreate.js | 28 +++++++++++++++++-- api/typeDefs/sub.js | 2 ++ components/reply.js | 3 +- components/territory-form.js | 8 ++++++ components/territory-header.js | 4 +++ fragments/items.js | 1 + fragments/paidAction.js | 8 +++--- fragments/subs.js | 1 + lib/validate.js | 3 ++ .../migration.sql | 2 ++ prisma/schema.prisma | 1 + 11 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 prisma/migrations/20241227132729_comment_fee_control/migration.sql diff --git a/api/paidAction/itemCreate.js b/api/paidAction/itemCreate.js index 9f6a23fb3..60769fe10 100644 --- a/api/paidAction/itemCreate.js +++ b/api/paidAction/itemCreate.js @@ -12,8 +12,32 @@ export const paymentMethods = [ ] export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio }, { models, me }) { - const sub = (parentId || bio) ? null : await models.sub.findUnique({ where: { name: subName } }) - const baseCost = sub ? satsToMsats(sub.baseCost) : 1000n + const BIO_COST = 1000n + async function findRootItem (models, parentId) { + const item = await models.item.findFirst({ + where: { id: Number(parentId) }, + select: { rootId: true } + }) + + return models.item.findFirst({ + where: { id: Number(item.rootId || parentId) }, + include: { sub: true } + }) + } + async function getBaseCost ({ models, bio, parentId, subName }) { + if (bio) return BIO_COST + + if (parentId) { + const rootItem = await findRootItem(models, parentId) + return rootItem.bio ? BIO_COST : satsToMsats(rootItem.sub?.replyCost) + } + + const sub = await models.sub.findUnique({ where: { name: subName } }) + return satsToMsats(sub.baseCost) + } + const baseCost = await getBaseCost({ models, bio, parentId, subName }) + // const sub = (parentId || bio) ? null : await models.sub.findUnique({ where: { name: subName } }) + // const baseCost = sub ? satsToMsats(sub.baseCost) : 1000n // cost = baseCost * 10^num_items_in_10m * 100 (anon) or 1 (user) + upload fees + boost const [{ cost }] = await models.$queryRaw` diff --git a/api/typeDefs/sub.js b/api/typeDefs/sub.js index 29fc8c7f5..4d550a413 100644 --- a/api/typeDefs/sub.js +++ b/api/typeDefs/sub.js @@ -16,6 +16,7 @@ export default gql` extend type Mutation { upsertSub(oldName: String, name: String!, desc: String, baseCost: Int!, + replyCost: Int!, postTypes: [String!]!, billingType: String!, billingAutoRenew: Boolean!, moderated: Boolean!, nsfw: Boolean!): SubPaidAction! @@ -45,6 +46,7 @@ export default gql` billedLastAt: Date! billPaidUntil: Date baseCost: Int! + replyCost: Int! status: String! moderated: Boolean! moderatedCount: Int! diff --git a/components/reply.js b/components/reply.js index 71427b471..c5aab3253 100644 --- a/components/reply.js +++ b/components/reply.js @@ -45,6 +45,7 @@ export default forwardRef(function Reply ({ const replyInput = useRef(null) const showModal = useShowModal() const root = useRoot() + const bio = root?.bio const sub = item?.sub || root?.sub useEffect(() => { @@ -173,7 +174,7 @@ export default forwardRef(function Reply ({ {reply && <div className={styles.reply}> <FeeButtonProvider - baseLineItems={postCommentBaseLineItems({ baseCost: 1, comment: true, me: !!me })} + baseLineItems={postCommentBaseLineItems({ baseCost: bio ? 1 : sub.replyCost, comment: true, me: !!me })} useRemoteLineItems={postCommentUseRemoteLineItems({ parentId: item.id, me: !!me })} > <Form diff --git a/components/territory-form.js b/components/territory-form.js index 55ce75f25..0d6347a85 100644 --- a/components/territory-form.js +++ b/components/territory-form.js @@ -91,6 +91,7 @@ export default function TerritoryForm ({ sub }) { name: sub?.name || '', desc: sub?.desc || '', baseCost: sub?.baseCost || 10, + replyCost: sub?.replyCost || 1, postTypes: sub?.postTypes || POST_TYPES, billingType: sub?.billingType || 'MONTHLY', billingAutoRenew: sub?.billingAutoRenew || false, @@ -136,6 +137,13 @@ export default function TerritoryForm ({ sub }) { required append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} /> + <Input + label='reply cost' + name='replyCost' + type='number' + required + append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} + /> <CheckboxGroup label='post types' name='postTypes'> <Row> <Col xs={4} sm='auto'> diff --git a/components/territory-header.js b/components/territory-header.js index 566a8afce..a53a99e8c 100644 --- a/components/territory-header.js +++ b/components/territory-header.js @@ -61,6 +61,10 @@ export function TerritoryInfo ({ sub }) { <span>post cost </span> <span className='fw-bold'>{numWithUnits(sub.baseCost)}</span> </div> + <div className='text-muted'> + <span>reply cost </span> + <span className='fw-bold'>{numWithUnits(sub.replyCost)}</span> + </div> <TerritoryBillingLine sub={sub} /> </CardFooter> </> diff --git a/fragments/items.js b/fragments/items.js index 6e1a9f407..66b1ef86e 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -34,6 +34,7 @@ export const ITEM_FIELDS = gql` meMuteSub meSubscription nsfw + replyCost } otsHash position diff --git a/fragments/paidAction.js b/fragments/paidAction.js index c47fa7005..a086887ff 100644 --- a/fragments/paidAction.js +++ b/fragments/paidAction.js @@ -250,10 +250,10 @@ export const UPDATE_COMMENT = gql` export const UPSERT_SUB = gql` ${PAID_ACTION} mutation upsertSub($oldName: String, $name: String!, $desc: String, $baseCost: Int!, - $postTypes: [String!]!, $billingType: String!, + $replyCost: Int!, $postTypes: [String!]!, $billingType: String!, $billingAutoRenew: Boolean!, $moderated: Boolean!, $nsfw: Boolean!) { upsertSub(oldName: $oldName, name: $name, desc: $desc, baseCost: $baseCost, - postTypes: $postTypes, billingType: $billingType, + replyCost: $replyCost, postTypes: $postTypes, billingType: $billingType, billingAutoRenew: $billingAutoRenew, moderated: $moderated, nsfw: $nsfw) { result { name @@ -265,10 +265,10 @@ export const UPSERT_SUB = gql` export const UNARCHIVE_TERRITORY = gql` ${PAID_ACTION} mutation unarchiveTerritory($name: String!, $desc: String, $baseCost: Int!, - $postTypes: [String!]!, $billingType: String!, + $replyCost: Int!, $postTypes: [String!]!, $billingType: String!, $billingAutoRenew: Boolean!, $moderated: Boolean!, $nsfw: Boolean!) { unarchiveTerritory(name: $name, desc: $desc, baseCost: $baseCost, - postTypes: $postTypes, billingType: $billingType, + replyCost: $replyCost, postTypes: $postTypes, billingType: $billingType, billingAutoRenew: $billingAutoRenew, moderated: $moderated, nsfw: $nsfw) { result { name diff --git a/fragments/subs.js b/fragments/subs.js index 8b84f8d78..1ff2c492e 100644 --- a/fragments/subs.js +++ b/fragments/subs.js @@ -25,6 +25,7 @@ export const SUB_FIELDS = gql` billedLastAt billPaidUntil baseCost + replyCost userId desc status diff --git a/lib/validate.js b/lib/validate.js index 86b03bded..fc238e181 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -317,6 +317,9 @@ export function territorySchema (args) { baseCost: intValidator .min(1, 'must be at least 1') .max(100000, 'must be at most 100k'), + replyCost: intValidator + .min(1, 'must be at least 1') + .max(100000, 'must be at most 100k'), postTypes: array().of(string().oneOf(POST_TYPES)).min(1, 'must support at least one post type'), billingType: string().required('required').oneOf(TERRITORY_BILLING_TYPES, 'required'), nsfw: boolean() diff --git a/prisma/migrations/20241227132729_comment_fee_control/migration.sql b/prisma/migrations/20241227132729_comment_fee_control/migration.sql new file mode 100644 index 000000000..241212a0b --- /dev/null +++ b/prisma/migrations/20241227132729_comment_fee_control/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Sub" ADD COLUMN "replyCost" INTEGER NOT NULL DEFAULT 1; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 59685b931..5063a3c98 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -729,6 +729,7 @@ model Sub { rankingType RankingType allowFreebies Boolean @default(true) baseCost Int @default(1) + replyCost Int @default(1) rewardsPct Int @default(50) desc String? status Status @default(ACTIVE) From f16b31c65218526deae6334e79ab7b4bbf0e1ef8 Mon Sep 17 00:00:00 2001 From: Soxasora <sora@soxa.dev> Date: Fri, 27 Dec 2024 17:30:55 +0100 Subject: [PATCH 2/7] update typeDefs for unarchiving territories --- api/typeDefs/sub.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/typeDefs/sub.js b/api/typeDefs/sub.js index 4d550a413..a7b00aeef 100644 --- a/api/typeDefs/sub.js +++ b/api/typeDefs/sub.js @@ -25,7 +25,7 @@ export default gql` toggleSubSubscription(name: String!): Boolean! transferTerritory(subName: String!, userName: String!): Sub unarchiveTerritory(name: String!, desc: String, baseCost: Int!, - postTypes: [String!]!, + replyCost: Int!, postTypes: [String!]!, billingType: String!, billingAutoRenew: Boolean!, moderated: Boolean!, nsfw: Boolean!): SubPaidAction! } From 72ed633f504033a42818a84aab7736a8253ac241 Mon Sep 17 00:00:00 2001 From: Soxasora <sora@soxa.dev> Date: Sun, 29 Dec 2024 11:45:47 +0100 Subject: [PATCH 3/7] review: move functions to top level; consider saloon items --- api/paidAction/itemCreate.js | 45 ++++++++++++++++++------------------ components/reply.js | 2 +- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/api/paidAction/itemCreate.js b/api/paidAction/itemCreate.js index 60769fe10..238323eb5 100644 --- a/api/paidAction/itemCreate.js +++ b/api/paidAction/itemCreate.js @@ -11,33 +11,34 @@ export const paymentMethods = [ PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC ] -export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio }, { models, me }) { - const BIO_COST = 1000n - async function findRootItem (models, parentId) { - const item = await models.item.findFirst({ - where: { id: Number(parentId) }, - select: { rootId: true } - }) +export const BIO_COST = 1000n - return models.item.findFirst({ - where: { id: Number(item.rootId || parentId) }, - include: { sub: true } - }) - } - async function getBaseCost ({ models, bio, parentId, subName }) { - if (bio) return BIO_COST +export async function findRootItem (models, parentId) { + const item = await models.item.findFirst({ + where: { id: Number(parentId) }, + select: { rootId: true } + }) - if (parentId) { - const rootItem = await findRootItem(models, parentId) - return rootItem.bio ? BIO_COST : satsToMsats(rootItem.sub?.replyCost) - } + return models.item.findFirst({ + where: { id: Number(item.rootId || parentId) }, + include: { sub: true } + }) +} - const sub = await models.sub.findUnique({ where: { name: subName } }) - return satsToMsats(sub.baseCost) +export async function getBaseCost ({ models, bio, parentId, subName }) { + if (bio) return BIO_COST + + if (parentId) { + const rootItem = await findRootItem(models, parentId) + return rootItem.bio ? BIO_COST : satsToMsats(rootItem.sub?.replyCost) } + + const sub = await models.sub.findUnique({ where: { name: subName } }) + return satsToMsats(sub.baseCost) +} + +export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio }, { models, me }) { const baseCost = await getBaseCost({ models, bio, parentId, subName }) - // const sub = (parentId || bio) ? null : await models.sub.findUnique({ where: { name: subName } }) - // const baseCost = sub ? satsToMsats(sub.baseCost) : 1000n // cost = baseCost * 10^num_items_in_10m * 100 (anon) or 1 (user) + upload fees + boost const [{ cost }] = await models.$queryRaw` diff --git a/components/reply.js b/components/reply.js index c5aab3253..093059048 100644 --- a/components/reply.js +++ b/components/reply.js @@ -174,7 +174,7 @@ export default forwardRef(function Reply ({ {reply && <div className={styles.reply}> <FeeButtonProvider - baseLineItems={postCommentBaseLineItems({ baseCost: bio ? 1 : sub.replyCost, comment: true, me: !!me })} + baseLineItems={postCommentBaseLineItems({ baseCost: bio || !sub ? 1 : sub.replyCost, comment: true, me: !!me })} useRemoteLineItems={postCommentUseRemoteLineItems({ parentId: item.id, me: !!me })} > <Form From 06e642f9e183016b3f9baf6b0187c200d74bb320 Mon Sep 17 00:00:00 2001 From: Soxasora <sora@soxa.dev> Date: Sun, 29 Dec 2024 11:48:56 +0100 Subject: [PATCH 4/7] ux: cleaner post/reply cost section --- components/territory-header.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/components/territory-header.js b/components/territory-header.js index a53a99e8c..8d64f0144 100644 --- a/components/territory-header.js +++ b/components/territory-header.js @@ -57,13 +57,16 @@ export function TerritoryInfo ({ sub }) { <span> on </span> <span className='fw-bold'>{new Date(sub.createdAt).toDateString()}</span> </div> - <div className='text-muted'> - <span>post cost </span> - <span className='fw-bold'>{numWithUnits(sub.baseCost)}</span> - </div> - <div className='text-muted'> - <span>reply cost </span> - <span className='fw-bold'>{numWithUnits(sub.replyCost)}</span> + <div className='d-flex'> + <div className='text-muted'> + <span>post cost </span> + <span className='fw-bold'>{numWithUnits(sub.baseCost)}</span> + </div> + <span className='px-1'> \ </span> + <div className='text-muted'> + <span>reply cost </span> + <span className='fw-bold'>{numWithUnits(sub.replyCost)}</span> + </div> </div> <TerritoryBillingLine sub={sub} /> </CardFooter> From c6a323ef636a5e96827c52f9e7e060c5174d6919 Mon Sep 17 00:00:00 2001 From: Soxasora <sora@soxa.dev> Date: Sun, 29 Dec 2024 21:27:10 +0100 Subject: [PATCH 5/7] hotfix: handle salon replies --- api/paidAction/itemCreate.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/paidAction/itemCreate.js b/api/paidAction/itemCreate.js index 238323eb5..c640f0c61 100644 --- a/api/paidAction/itemCreate.js +++ b/api/paidAction/itemCreate.js @@ -11,7 +11,7 @@ export const paymentMethods = [ PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC ] -export const BIO_COST = 1000n +export const ITEM_COST = 1000n export async function findRootItem (models, parentId) { const item = await models.item.findFirst({ @@ -26,11 +26,13 @@ export async function findRootItem (models, parentId) { } export async function getBaseCost ({ models, bio, parentId, subName }) { - if (bio) return BIO_COST + if (bio) return ITEM_COST if (parentId) { const rootItem = await findRootItem(models, parentId) - return rootItem.bio ? BIO_COST : satsToMsats(rootItem.sub?.replyCost) + + if (rootItem.bio || !rootItem.sub) return ITEM_COST + return satsToMsats(rootItem.sub.replyCost) } const sub = await models.sub.findUnique({ where: { name: subName } }) From a51f2ff4de0f1a6df0a9111a80a931e9e39f3355 Mon Sep 17 00:00:00 2001 From: k00b <k00b@stacker.news> Date: Thu, 6 Feb 2025 20:17:48 -0600 Subject: [PATCH 6/7] bios don't have subs + simplify root query --- api/paidAction/itemCreate.js | 31 ++++++++++++++----------------- components/reply.js | 3 +-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/api/paidAction/itemCreate.js b/api/paidAction/itemCreate.js index a7f0418f9..be7a77d60 100644 --- a/api/paidAction/itemCreate.js +++ b/api/paidAction/itemCreate.js @@ -13,28 +13,25 @@ export const paymentMethods = [ PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC ] -export const ITEM_COST = 1000n - -export async function findRootItem (models, parentId) { - const item = await models.item.findFirst({ - where: { id: Number(parentId) }, - select: { rootId: true } - }) - - return models.item.findFirst({ - where: { id: Number(item.rootId || parentId) }, - include: { sub: true } - }) -} +export const DEFAULT_ITEM_COST = 1000n export async function getBaseCost ({ models, bio, parentId, subName }) { - if (bio) return ITEM_COST + if (bio) return DEFAULT_ITEM_COST if (parentId) { - const rootItem = await findRootItem(models, parentId) + // the subname is stored in the root item of the thread + const parent = await models.item.findFirst({ + where: { id: Number(parentId) }, + include: { + root: { include: { sub: true } }, + sub: true + } + }) + + const root = parent.root ?? parent - if (rootItem.bio || !rootItem.sub) return ITEM_COST - return satsToMsats(rootItem.sub.replyCost) + if (!root.sub) return DEFAULT_ITEM_COST + return satsToMsats(root.sub.replyCost) } const sub = await models.sub.findUnique({ where: { name: subName } }) diff --git a/components/reply.js b/components/reply.js index da79fdaf2..ec12d0a5f 100644 --- a/components/reply.js +++ b/components/reply.js @@ -28,7 +28,6 @@ export default forwardRef(function Reply ({ const replyInput = useRef(null) const showModal = useShowModal() const root = useRoot() - const bio = root?.bio const sub = item?.sub || root?.sub useEffect(() => { @@ -162,7 +161,7 @@ export default forwardRef(function Reply ({ {reply && <div className={styles.reply}> <FeeButtonProvider - baseLineItems={postCommentBaseLineItems({ baseCost: bio || !sub ? 1 : sub.replyCost, comment: true, me: !!me })} + baseLineItems={postCommentBaseLineItems({ baseCost: sub?.replyCost ?? 1, comment: true, me: !!me })} useRemoteLineItems={postCommentUseRemoteLineItems({ parentId: item.id, me: !!me })} > <Form From dae5430c82a468feb2a4dc3b6c1331b340c5a819 Mon Sep 17 00:00:00 2001 From: k00b <k00b@stacker.news> Date: Thu, 6 Feb 2025 20:18:03 -0600 Subject: [PATCH 7/7] move reply cost to accordian --- components/territory-form.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/components/territory-form.js b/components/territory-form.js index 0d6347a85..0983002c8 100644 --- a/components/territory-form.js +++ b/components/territory-form.js @@ -137,13 +137,6 @@ export default function TerritoryForm ({ sub }) { required append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} /> - <Input - label='reply cost' - name='replyCost' - type='number' - required - append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} - /> <CheckboxGroup label='post types' name='postTypes'> <Row> <Col xs={4} sm='auto'> @@ -242,6 +235,13 @@ export default function TerritoryForm ({ sub }) { header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>options</div>} body={ <> + <Input + label='reply cost' + name='replyCost' + type='number' + required + append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} + /> <BootstrapForm.Label>moderation</BootstrapForm.Label> <Checkbox inline