feat: comment fee control (#1768)

* feat: comment fee control

* update typeDefs for unarchiving territories

* review: move functions to top level; consider saloon items

* ux: cleaner post/reply cost section

* hotfix: handle salon replies

* bios don't have subs + simplify root query

* move reply cost to accordian

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
soxa 2025-02-07 20:38:57 +01:00 committed by GitHub
parent ac321be3cd
commit 5c2aa979ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 60 additions and 11 deletions

View File

@ -13,9 +13,33 @@ export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
] ]
export const DEFAULT_ITEM_COST = 1000n
export async function getBaseCost ({ models, bio, parentId, subName }) {
if (bio) return DEFAULT_ITEM_COST
if (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 (!root.sub) return DEFAULT_ITEM_COST
return satsToMsats(root.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 }) { 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 = await getBaseCost({ models, bio, parentId, subName })
const baseCost = sub ? satsToMsats(sub.baseCost) : 1000n
// cost = baseCost * 10^num_items_in_10m * 100 (anon) or 1 (user) + upload fees + boost // cost = baseCost * 10^num_items_in_10m * 100 (anon) or 1 (user) + upload fees + boost
const [{ cost }] = await models.$queryRaw` const [{ cost }] = await models.$queryRaw`

View File

@ -16,6 +16,7 @@ export default gql`
extend type Mutation { extend type Mutation {
upsertSub(oldName: String, name: String!, desc: String, baseCost: Int!, upsertSub(oldName: String, name: String!, desc: String, baseCost: Int!,
replyCost: Int!,
postTypes: [String!]!, postTypes: [String!]!,
billingType: String!, billingAutoRenew: Boolean!, billingType: String!, billingAutoRenew: Boolean!,
moderated: Boolean!, nsfw: Boolean!): SubPaidAction! moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
@ -24,7 +25,7 @@ export default gql`
toggleSubSubscription(name: String!): Boolean! toggleSubSubscription(name: String!): Boolean!
transferTerritory(subName: String!, userName: String!): Sub transferTerritory(subName: String!, userName: String!): Sub
unarchiveTerritory(name: String!, desc: String, baseCost: Int!, unarchiveTerritory(name: String!, desc: String, baseCost: Int!,
postTypes: [String!]!, replyCost: Int!, postTypes: [String!]!,
billingType: String!, billingAutoRenew: Boolean!, billingType: String!, billingAutoRenew: Boolean!,
moderated: Boolean!, nsfw: Boolean!): SubPaidAction! moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
} }
@ -45,6 +46,7 @@ export default gql`
billedLastAt: Date! billedLastAt: Date!
billPaidUntil: Date billPaidUntil: Date
baseCost: Int! baseCost: Int!
replyCost: Int!
status: String! status: String!
moderated: Boolean! moderated: Boolean!
moderatedCount: Int! moderatedCount: Int!

View File

@ -161,7 +161,7 @@ export default forwardRef(function Reply ({
{reply && {reply &&
<div className={styles.reply}> <div className={styles.reply}>
<FeeButtonProvider <FeeButtonProvider
baseLineItems={postCommentBaseLineItems({ baseCost: 1, comment: true, me: !!me })} baseLineItems={postCommentBaseLineItems({ baseCost: sub?.replyCost ?? 1, comment: true, me: !!me })}
useRemoteLineItems={postCommentUseRemoteLineItems({ parentId: item.id, me: !!me })} useRemoteLineItems={postCommentUseRemoteLineItems({ parentId: item.id, me: !!me })}
> >
<Form <Form

View File

@ -91,6 +91,7 @@ export default function TerritoryForm ({ sub }) {
name: sub?.name || '', name: sub?.name || '',
desc: sub?.desc || '', desc: sub?.desc || '',
baseCost: sub?.baseCost || 10, baseCost: sub?.baseCost || 10,
replyCost: sub?.replyCost || 1,
postTypes: sub?.postTypes || POST_TYPES, postTypes: sub?.postTypes || POST_TYPES,
billingType: sub?.billingType || 'MONTHLY', billingType: sub?.billingType || 'MONTHLY',
billingAutoRenew: sub?.billingAutoRenew || false, billingAutoRenew: sub?.billingAutoRenew || false,
@ -234,6 +235,13 @@ export default function TerritoryForm ({ sub }) {
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>options</div>} header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>options</div>}
body={ body={
<> <>
<Input
label='reply cost'
name='replyCost'
type='number'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<BootstrapForm.Label>moderation</BootstrapForm.Label> <BootstrapForm.Label>moderation</BootstrapForm.Label>
<Checkbox <Checkbox
inline inline

View File

@ -57,9 +57,16 @@ export function TerritoryInfo ({ sub }) {
<span> on </span> <span> on </span>
<span className='fw-bold'>{new Date(sub.createdAt).toDateString()}</span> <span className='fw-bold'>{new Date(sub.createdAt).toDateString()}</span>
</div> </div>
<div className='text-muted'> <div className='d-flex'>
<span>post cost </span> <div className='text-muted'>
<span className='fw-bold'>{numWithUnits(sub.baseCost)}</span> <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> </div>
<TerritoryBillingLine sub={sub} /> <TerritoryBillingLine sub={sub} />
</CardFooter> </CardFooter>

View File

@ -35,6 +35,7 @@ export const ITEM_FIELDS = gql`
meMuteSub meMuteSub
meSubscription meSubscription
nsfw nsfw
replyCost
} }
otsHash otsHash
position position

View File

@ -264,10 +264,10 @@ export const UPDATE_COMMENT = gql`
export const UPSERT_SUB = gql` export const UPSERT_SUB = gql`
${PAID_ACTION} ${PAID_ACTION}
mutation upsertSub($oldName: String, $name: String!, $desc: String, $baseCost: Int!, 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!) { $billingAutoRenew: Boolean!, $moderated: Boolean!, $nsfw: Boolean!) {
upsertSub(oldName: $oldName, name: $name, desc: $desc, baseCost: $baseCost, 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) { billingAutoRenew: $billingAutoRenew, moderated: $moderated, nsfw: $nsfw) {
result { result {
name name
@ -279,10 +279,10 @@ export const UPSERT_SUB = gql`
export const UNARCHIVE_TERRITORY = gql` export const UNARCHIVE_TERRITORY = gql`
${PAID_ACTION} ${PAID_ACTION}
mutation unarchiveTerritory($name: String!, $desc: String, $baseCost: Int!, mutation unarchiveTerritory($name: String!, $desc: String, $baseCost: Int!,
$postTypes: [String!]!, $billingType: String!, $replyCost: Int!, $postTypes: [String!]!, $billingType: String!,
$billingAutoRenew: Boolean!, $moderated: Boolean!, $nsfw: Boolean!) { $billingAutoRenew: Boolean!, $moderated: Boolean!, $nsfw: Boolean!) {
unarchiveTerritory(name: $name, desc: $desc, baseCost: $baseCost, unarchiveTerritory(name: $name, desc: $desc, baseCost: $baseCost,
postTypes: $postTypes, billingType: $billingType, replyCost: $replyCost, postTypes: $postTypes, billingType: $billingType,
billingAutoRenew: $billingAutoRenew, moderated: $moderated, nsfw: $nsfw) { billingAutoRenew: $billingAutoRenew, moderated: $moderated, nsfw: $nsfw) {
result { result {
name name

View File

@ -25,6 +25,7 @@ export const SUB_FIELDS = gql`
billedLastAt billedLastAt
billPaidUntil billPaidUntil
baseCost baseCost
replyCost
userId userId
desc desc
status status

View File

@ -317,6 +317,9 @@ export function territorySchema (args) {
baseCost: intValidator baseCost: intValidator
.min(1, 'must be at least 1') .min(1, 'must be at least 1')
.max(100000, 'must be at most 100k'), .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'), 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'), billingType: string().required('required').oneOf(TERRITORY_BILLING_TYPES, 'required'),
nsfw: boolean() nsfw: boolean()

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Sub" ADD COLUMN "replyCost" INTEGER NOT NULL DEFAULT 1;

View File

@ -737,6 +737,7 @@ model Sub {
rankingType RankingType rankingType RankingType
allowFreebies Boolean @default(true) allowFreebies Boolean @default(true)
baseCost Int @default(1) baseCost Int @default(1)
replyCost Int @default(1)
rewardsPct Int @default(50) rewardsPct Int @default(50)
desc String? desc String?
status Status @default(ACTIVE) status Status @default(ACTIVE)