allow territory founders to disable freebies

This commit is contained in:
keyan 2023-12-10 15:41:20 -06:00
parent 11e6a98bc7
commit c8bca3ef63
8 changed files with 159 additions and 16 deletions

View File

@ -9,8 +9,9 @@ export default gql`
extend type Mutation {
upsertSub(name: String!, desc: String, baseCost: Int!,
postTypes: [String!]!, billingType: String!, billingAutoRenew: Boolean!,
hash: String, hmac: String): Sub
postTypes: [String!]!, allowFreebies: Boolean!,
billingType: String!, billingAutoRenew: Boolean!,
hash: String, hmac: String): Sub
paySub(name: String!, hash: String, hmac: String): Sub
}
@ -22,6 +23,7 @@ export default gql`
desc: String
updatedAt: Date!
postTypes: [String!]!
allowFreebies: Boolean!
billingCost: Int!
billingType: String!
billingAutoRenew: Boolean!

View File

@ -14,7 +14,7 @@ import { SubmitButton } from './form'
const FeeButtonContext = createContext()
export function postCommentBaseLineItems ({ baseCost = 1, comment = false, me }) {
export function postCommentBaseLineItems ({ baseCost = 1, comment = false, allowFreebies = true, me }) {
// XXX this doesn't match the logic on the server but it has the same
// result on fees ... will need to change the server logic to match
const anonCharge = me
@ -30,14 +30,14 @@ export function postCommentBaseLineItems ({ baseCost = 1, comment = false, me })
baseCost: {
term: baseCost,
label: `${comment ? 'comment' : 'post'} cost`,
modifier: (cost) => cost + baseCost
modifier: (cost) => cost + baseCost,
allowFreebies
},
...anonCharge
}
}
export function postCommentUseRemoteLineItems ({ parentId, me } = {}) {
if (!me) return () => {}
export function postCommentUseRemoteLineItems ({ parentId } = {}) {
const query = parentId
? gql`{ itemRepetition(parentId: "${parentId}") }`
: gql`{ itemRepetition }`
@ -114,18 +114,19 @@ function FreebieDialog () {
export default function FeeButton ({ ChildButton = SubmitButton, variant, text, disabled }) {
const me = useMe()
const { lines, total, disabled: ctxDisabled } = useFeeButton()
// freebies: there's only a base cost, it's less than 10, and we have less than 10 sats
const free = total === lines.baseCost?.modifier(0) && me?.privates?.sats < total
// freebies: there's only a base cost and we don't have enough sats
const free = total === lines.baseCost?.modifier(0) && lines.baseCost?.allowFreebies && me?.privates?.sats < total
const feeText = free
? 'free'
: total > 1
? numWithUnits(total, { abbreviate: false, format: true })
: undefined
disabled ||= ctxDisabled
return (
<div className={styles.feeButton}>
<ActionTooltip overlayText={feeText}>
<ChildButton variant={variant} disabled={disabled || ctxDisabled}>{text}{feeText && <small> {feeText}</small>}</ChildButton>
<ChildButton variant={variant} disabled={disabled} nonDisabledText={feeText}>{text}</ChildButton>
</ActionTooltip>
{!me && <AnonInfo />}
{(free && <Info><FreebieDialog /></Info>) ||

View File

@ -31,15 +31,17 @@ import { useFeeButton } from './fee-button'
import Thumb from '../svgs/thumb-up-fill.svg'
export function SubmitButton ({
children, variant, value, onClick, disabled, ...props
children, variant, value, onClick, disabled, nonDisabledText, ...props
}) {
const formik = useFormikContext()
disabled ||= formik.isSubmitting
return (
<Button
variant={variant || 'main'}
type='submit'
disabled={disabled || formik.isSubmitting}
disabled={disabled}
onClick={value
? e => {
formik.setFieldValue('submit', value)
@ -48,7 +50,7 @@ export function SubmitButton ({
: onClick}
{...props}
>
{children}
{children}{!disabled && nonDisabledText && <small> {nonDisabledText}</small>}
</Button>
)
}

View File

@ -138,7 +138,7 @@ export function PostForm ({ type, sub, children }) {
return (
<FeeButtonProvider
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, me: !!me }) : undefined}
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, allowFreebies: sub.allowFreebies, me: !!me }) : undefined}
useRemoteLineItems={postCommentUseRemoteLineItems({ me: !!me })}
>
<FormType sub={sub}>{children}</FormType>

View File

@ -15,10 +15,10 @@ export default function TerritoryForm ({ sub }) {
const [upsertSub] = useMutation(
gql`
mutation upsertSub($name: String!, $desc: String, $baseCost: Int!,
$postTypes: [String!]!, $billingType: String!, $billingAutoRenew: Boolean!,
$hash: String, $hmac: String) {
$postTypes: [String!]!, $allowFreebies: Boolean!, $billingType: String!,
$billingAutoRenew: Boolean!, $hash: String, $hmac: String) {
upsertSub(name: $name, desc: $desc, baseCost: $baseCost,
postTypes: $postTypes, billingType: $billingType,
postTypes: $postTypes, allowFreebies: $allowFreebies, billingType: $billingType,
billingAutoRenew: $billingAutoRenew, hash: $hash, hmac: $hmac) {
name
}
@ -61,6 +61,7 @@ export default function TerritoryForm ({ sub }) {
desc: sub?.desc || '',
baseCost: sub?.baseCost || 10,
postTypes: sub?.postTypes || POST_TYPES,
allowFreebies: typeof sub?.allowFreebies === 'undefined' ? true : sub?.allowFreebies,
billingType: sub?.billingType || 'MONTHLY',
billingAutoRenew: sub?.billingAutoRenew || false
}}
@ -98,9 +99,15 @@ export default function TerritoryForm ({ sub }) {
label='post cost'
name='baseCost'
type='number'
groupClassName='mb-2'
required
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<Checkbox
label='allow free posts'
name='allowFreebies'
groupClassName='ms-1'
/>
<CheckboxGroup label='post types' name='postTypes'>
<Row>
<Col xs={4} sm='auto'>

View File

@ -6,6 +6,7 @@ export const SUB_FIELDS = gql`
fragment SubFields on Sub {
name
postTypes
allowFreebies
rankingType
billingType
billingCost

View File

@ -0,0 +1,129 @@
-- AlterTable
ALTER TABLE "Sub" ADD COLUMN "allowFreebies" BOOLEAN NOT NULL DEFAULT true;
-- make freebies optional
CREATE OR REPLACE FUNCTION create_item(
jitem JSONB, forward JSONB, poll_options JSONB, spam_within INTERVAL, upload_ids INTEGER[])
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
user_msats BIGINT;
cost_msats BIGINT := 1000;
base_cost_msats BIGINT := 1000;
freebie BOOLEAN;
allow_freebies BOOLEAN := true;
item "Item";
med_votes FLOAT;
select_clause TEXT;
BEGIN
PERFORM ASSERT_SERIALIZED();
-- access fields with appropriate types
item := jsonb_populate_record(NULL::"Item", jitem);
SELECT msats INTO user_msats FROM users WHERE id = item."userId";
-- if this is a post, get the base cost of the sub
IF item."parentId" IS NULL AND item."subName" IS NOT NULL THEN
SELECT "baseCost" * 1000, "baseCost" * 1000, "allowFreebies"
INTO base_cost_msats, cost_msats, allow_freebies
FROM "Sub"
WHERE name = item."subName";
END IF;
IF item."maxBid" IS NULL THEN
-- spam multiplier
cost_msats := cost_msats * POWER(10, item_spam(item."parentId", item."userId", spam_within));
END IF;
-- add image fees
IF upload_ids IS NOT NULL THEN
cost_msats := cost_msats + (SELECT "nUnpaid" * "imageFeeMsats" FROM image_fees_info(item."userId", upload_ids));
UPDATE "Upload" SET paid = 't' WHERE id = ANY(upload_ids);
END IF;
-- it's only a freebie if it's no greater than the base cost, they have less than the cost, and boost = 0
freebie := allow_freebies AND (cost_msats <= base_cost_msats) AND (user_msats < cost_msats) AND (item.boost IS NULL OR item.boost = 0);
IF NOT freebie AND cost_msats > user_msats THEN
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
END IF;
-- get this user's median item score
SELECT COALESCE(
percentile_cont(0.5) WITHIN GROUP(
ORDER BY "weightedVotes" - "weightedDownVotes"), 0)
INTO med_votes FROM "Item" WHERE "userId" = item."userId";
-- if their median votes are positive, start at 0
-- if the median votes are negative, start their post with that many down votes
-- basically: if their median post is bad, presume this post is too
-- addendum: if they're an anon poster, always start at 0
IF med_votes >= 0 OR item."userId" = 27 THEN
med_votes := 0;
ELSE
med_votes := ABS(med_votes);
END IF;
-- there's no great way to set default column values when using json_populate_record
-- so we need to only select fields with non-null values that way when func input
-- does not include a value, the default value is used instead of null
SELECT string_agg(quote_ident(key), ',') INTO select_clause
FROM jsonb_object_keys(jsonb_strip_nulls(jitem)) k(key);
-- insert the item
EXECUTE format($fmt$
INSERT INTO "Item" (%s, "weightedDownVotes", freebie)
SELECT %1$s, %L, %L
FROM jsonb_populate_record(NULL::"Item", %L) RETURNING *
$fmt$, select_clause, med_votes, freebie, jitem) INTO item;
INSERT INTO "ItemForward" ("itemId", "userId", "pct")
SELECT item.id, "userId", "pct" FROM jsonb_populate_recordset(NULL::"ItemForward", forward);
-- Automatically subscribe to one's own posts
INSERT INTO "ThreadSubscription" ("itemId", "userId")
VALUES (item.id, item."userId");
-- Automatically subscribe forward recipients to the new post
INSERT INTO "ThreadSubscription" ("itemId", "userId")
SELECT item.id, "userId" FROM jsonb_populate_recordset(NULL::"ItemForward", forward);
INSERT INTO "PollOption" ("itemId", "option")
SELECT item.id, "option" FROM jsonb_array_elements_text(poll_options) o("option");
IF NOT freebie THEN
UPDATE users SET msats = msats - cost_msats WHERE id = item."userId";
INSERT INTO "ItemAct" (msats, "itemId", "userId", act)
VALUES (cost_msats, item.id, item."userId", 'FEE');
END IF;
-- if this item has boost
IF item.boost > 0 THEN
PERFORM item_act(item.id, item."userId", 'BOOST', item.boost);
END IF;
-- if this is a job
IF item."maxBid" IS NOT NULL THEN
PERFORM run_auction(item.id);
END IF;
-- if this is a bio
IF item.bio THEN
UPDATE users SET "bioId" = item.id WHERE id = item."userId";
END IF;
-- record attachments
IF upload_ids IS NOT NULL THEN
INSERT INTO "ItemUpload" ("itemId", "uploadId")
SELECT item.id, * FROM UNNEST(upload_ids);
END IF;
-- schedule imgproxy job
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
VALUES ('imgproxy', jsonb_build_object('id', item.id), 21, true, now() + interval '5 seconds');
RETURN item;
END;
$$;

View File

@ -405,6 +405,7 @@ model Sub {
postTypes PostType[]
rankingType RankingType
allowFreebies Boolean @default(true)
baseCost Int @default(1)
rewardsPct Int @default(50)
desc String?