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
]
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 }) {
const sub = (parentId || bio) ? null : await models.sub.findUnique({ where: { name: subName } })
const baseCost = sub ? satsToMsats(sub.baseCost) : 1000n
const baseCost = await getBaseCost({ models, bio, parentId, subName })
// cost = baseCost * 10^num_items_in_10m * 100 (anon) or 1 (user) + upload fees + boost
const [{ cost }] = await models.$queryRaw`

View File

@ -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!
@ -24,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!
}
@ -45,6 +46,7 @@ export default gql`
billedLastAt: Date!
billPaidUntil: Date
baseCost: Int!
replyCost: Int!
status: String!
moderated: Boolean!
moderatedCount: Int!

View File

@ -161,7 +161,7 @@ export default forwardRef(function Reply ({
{reply &&
<div className={styles.reply}>
<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 })}
>
<Form

View File

@ -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,
@ -234,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

View File

@ -57,9 +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 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>

View File

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

View File

@ -264,10 +264,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
@ -279,10 +279,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

View File

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

View File

@ -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()

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
allowFreebies Boolean @default(true)
baseCost Int @default(1)
replyCost Int @default(1)
rewardsPct Int @default(50)
desc String?
status Status @default(ACTIVE)