add freebies

This commit is contained in:
keyan 2022-09-27 16:19:15 -05:00
parent 1621eeac80
commit d9d426e5c3
21 changed files with 325 additions and 68 deletions

View File

@ -8,7 +8,6 @@ import {
BOOST_MIN, ITEM_SPAM_INTERVAL, MAX_POLL_NUM_CHOICES,
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST
} from '../../lib/constants'
import { mdHas } from '../../lib/md'
async function comments (me, models, id, sort) {
let orderBy
@ -85,15 +84,28 @@ export async function orderByNumerator (me, models) {
}
export async function filterClause (me, models) {
// by default don't include freebies unless they have upvotes
let clause = ' AND (NOT "Item".freebie OR "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0'
if (me) {
const user = await models.user.findUnique({ where: { id: me.id } })
// wild west mode has everything
if (user.wildWestMode) {
return ''
}
// greeter mode includes freebies if feebies haven't been flagged
if (user.greeterMode) {
clause = 'AND (NOT "Item".freebie OR ("Item"."weightedVotes" - "Item"."weightedDownVotes" >= 0 AND "Item".freebie)'
}
// always include if it's mine
clause += ` OR "Item"."userId" = ${me.id})`
} else {
// close default freebie clause
clause += ')'
}
// if the item is above the threshold or is mine
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
clause += ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
if (me) {
clause += ` OR "Item"."userId" = ${me.id}`
}
@ -215,7 +227,7 @@ export default {
${SELECT}
FROM "Item"
WHERE "parentId" IS NULL AND "Item".created_at <= $1 AND "Item".created_at > $3
AND "pinId" IS NULL
AND "pinId" IS NULL AND NOT bio
${subClause(4)}
${await filterClause(me, models)}
${await newTimedOrderByWeightedSats(me, models, 1)}
@ -228,7 +240,7 @@ export default {
${SELECT}
FROM "Item"
WHERE "parentId" IS NULL AND "Item".created_at <= $1
AND "pinId" IS NULL
AND "pinId" IS NULL AND NOT bio
${subClause(3)}
${await filterClause(me, models)}
${await newTimedOrderByWeightedSats(me, models, 1)}
@ -312,6 +324,21 @@ export default {
items
}
},
freebieItems: async (parent, { cursor }, { me, models }) => {
const decodedCursor = decodeCursor(cursor)
const items = await models.$queryRaw(`
${SELECT}
FROM "Item"
WHERE "Item".freebie
ORDER BY created_at DESC
OFFSET $1
LIMIT ${LIMIT}`, decodedCursor.offset)
return {
cursor: items.length === LIMIT ? nextCursorEncoded(decodedCursor) : null,
items
}
},
moreFlatComments: async (parent, { cursor, name, sort, within }, { me, models }) => {
const decodedCursor = decodeCursor(cursor)
@ -574,8 +601,6 @@ export default {
}
}
const hasImgLink = !!(text && mdHas(text, ['link', 'image']))
if (id) {
const optionCount = await models.pollOption.count({
where: {
@ -588,8 +613,8 @@ export default {
}
const [item] = await serialize(models,
models.$queryRaw(`${SELECT} FROM update_poll($1, $2, $3, $4, $5, $6, $7) AS "Item"`,
Number(id), title, text, Number(boost || 0), options, Number(fwdUser?.id), hasImgLink))
models.$queryRaw(`${SELECT} FROM update_poll($1, $2, $3, $4, $5, $6) AS "Item"`,
Number(id), title, text, Number(boost || 0), options, Number(fwdUser?.id)))
return item
} else {
@ -598,8 +623,8 @@ export default {
}
const [item] = await serialize(models,
models.$queryRaw(`${SELECT} FROM create_poll($1, $2, $3, $4, $5, $6, $7, $8, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
title, text, 1, Number(boost || 0), Number(me.id), options, Number(fwdUser?.id), hasImgLink))
models.$queryRaw(`${SELECT} FROM create_poll($1, $2, $3, $4, $5, $6, $7, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
title, text, 1, Number(boost || 0), Number(me.id), options, Number(fwdUser?.id)))
await createMentions(item, models)
@ -981,12 +1006,10 @@ export const updateItem = async (parent, { id, data: { title, url, text, boost,
}
}
const hasImgLink = !!(text && mdHas(text, ['link', 'image']))
const [item] = await serialize(models,
models.$queryRaw(
`${SELECT} FROM update_item($1, $2, $3, $4, $5, $6, $7) AS "Item"`,
Number(id), title, url, text, Number(boost || 0), Number(fwdUser?.id), hasImgLink))
`${SELECT} FROM update_item($1, $2, $3, $4, $5, $6) AS "Item"`,
Number(id), title, url, text, Number(boost || 0), Number(fwdUser?.id)))
await createMentions(item, models)
@ -1014,13 +1037,11 @@ const createItem = async (parent, { title, url, text, boost, forward, parentId }
}
}
const hasImgLink = !!(text && mdHas(text, ['link', 'image']))
const [item] = await serialize(models,
models.$queryRaw(
`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6, $7, $8, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6, $7, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
title, url, text, Number(boost || 0), Number(parentId), Number(me.id),
Number(fwdUser?.id), hasImgLink))
Number(fwdUser?.id)))
await createMentions(item, models)
@ -1058,9 +1079,9 @@ export const SELECT =
`SELECT "Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title,
"Item".text, "Item".url, "Item"."userId", "Item"."fwdUserId", "Item"."parentId", "Item"."pinId", "Item"."maxBid",
"Item".company, "Item".location, "Item".remote,
"Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost", "Item"."paidImgLink",
"Item"."subName", "Item".status, "Item"."uploadId", "Item"."pollCost",
"Item".sats, "Item".ncomments, "Item"."commentSats", "Item"."lastCommentAt", "Item"."weightedVotes",
"Item"."weightedDownVotes", ltree2text("Item"."path") AS "path"`
"Item"."weightedDownVotes", "Item".freebie, ltree2text("Item"."path") AS "path"`
async function newTimedOrderByWeightedSats (me, models, num) {
return `

View File

@ -1,6 +1,5 @@
import { AuthenticationError, UserInputError } from 'apollo-server-errors'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { mdHas } from '../../lib/md'
import { createMentions, getItem, SELECT, updateItem, filterClause } from './item'
import serialize from './serial'
@ -202,11 +201,9 @@ export default {
if (user.bioId) {
await updateItem(parent, { id: user.bioId, data: { text: bio, title: `@${user.name}'s bio` } }, { me, models })
} else {
const hasImgLink = !!(bio && mdHas(bio, ['link', 'image']))
const [item] = await serialize(models,
models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3, $4) AS "Item"`,
`@${user.name}'s bio`, bio, Number(me.id), hasImgLink))
models.$queryRaw(`${SELECT} FROM create_bio($1, $2, $3) AS "Item"`,
`@${user.name}'s bio`, bio, Number(me.id)))
await createMentions(item, models)
}

View File

@ -14,6 +14,7 @@ export default gql`
itemRepetition(parentId: ID): Int!
outlawedItems(cursor: String): Items
borderlandItems(cursor: String): Items
freebieItems(cursor: String): Items
}
type ItemActResult {
@ -83,6 +84,7 @@ export default gql`
meSats: Int!
meDontLike: Boolean!
outlawed: Boolean!
freebie: Boolean!
paidImgLink: Boolean
ncomments: Int!
comments: [Item!]!

View File

@ -31,7 +31,8 @@ export default gql`
setName(name: String!): Boolean
setSettings(tipDefault: Int!, noteItemSats: Boolean!, noteEarning: Boolean!,
noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!, wildWestMode: Boolean!): User
noteInvites: Boolean!, noteJobIndicator: Boolean!, hideInvoiceDesc: Boolean!,
wildWestMode: Boolean!, greeterMode: Boolean!): User
setPhoto(photoId: ID!): Int!
upsertBio(bio: String!): User!
setWalkthrough(tipPopover: Boolean, upvotePopover: Boolean): Boolean
@ -73,6 +74,7 @@ export default gql`
noteJobIndicator: Boolean!
hideInvoiceDesc: Boolean!
wildWestMode: Boolean!
greeterMode: Boolean!
lastCheckedJobs: String
authMethods: AuthMethods!
}

View File

@ -3,7 +3,6 @@ import * as Yup from 'yup'
import { gql, useMutation } from '@apollo/client'
import styles from './reply.module.css'
import TextareaAutosize from 'react-textarea-autosize'
import { useState } from 'react'
import { EditFeeButton } from './fee-button'
export const CommentSchema = Yup.object({
@ -11,14 +10,11 @@ export const CommentSchema = Yup.object({
})
export default function CommentEdit ({ comment, editThreshold, onSuccess, onCancel }) {
const [hasImgLink, setHasImgLink] = useState()
const [updateComment] = useMutation(
gql`
mutation updateComment($id: ID! $text: String!) {
updateComment(id: $id, text: $text) {
text
paidImgLink
}
}`, {
update (cache, { data: { updateComment } }) {
@ -27,9 +23,6 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
fields: {
text () {
return updateComment.text
},
paidImgLink () {
return updateComment.paidImgLink
}
}
})
@ -59,11 +52,10 @@ export default function CommentEdit ({ comment, editThreshold, onSuccess, onCanc
as={TextareaAutosize}
minRows={6}
autoFocus
setHasImgLink={setHasImgLink}
required
/>
<EditFeeButton
paidSats={comment.meSats} hadImgLink={comment.paidImgLink} hasImgLink={hasImgLink}
paidSats={comment.meSats}
parentId={comment.parentId} text='save' ChildButton={SubmitButton} variant='secondary'
/>
</Form>

View File

@ -133,8 +133,9 @@ export default function Comment ({
<a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a>
</Link>
{includeParent && <Parent item={item} rootText={rootText} />}
{me && !item.meSats && !item.meDontLike && <DontLikeThis id={item.id} />}
{item.outlawed && <Link href='/outlawed'><a>{' '}<Badge className={itemStyles.newComment} variant={null}>OUTLAWED</Badge></a></Link>}
{me && !item.meSats && !item.meDontLike && !item.mine && <DontLikeThis id={item.id} />}
{(item.outlawed && <Link href='/outlawed'><a>{' '}<Badge className={itemStyles.newComment} variant={null}>OUTLAWED</Badge></a></Link>) ||
(item.freebie && !item.mine && (me?.greeterMode) && <Link href='/freebie'><a>{' '}<Badge className={itemStyles.newComment} variant={null}>FREEBIE</Badge></a></Link>)}
{canEdit &&
<>
<span> \ </span>

View File

@ -6,7 +6,6 @@ import TextareaAutosize from 'react-textarea-autosize'
import Countdown from './countdown'
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
import { MAX_TITLE_LENGTH } from '../lib/constants'
import { useState } from 'react'
import FeeButton, { EditFeeButton } from './fee-button'
export function DiscussionForm ({
@ -16,7 +15,6 @@ export function DiscussionForm ({
}) {
const router = useRouter()
const client = useApolloClient()
const [hasImgLink, setHasImgLink] = useState()
// const me = useMe()
const [upsertDiscussion] = useMutation(
gql`
@ -77,17 +75,16 @@ export function DiscussionForm ({
hint={editThreshold
? <div className='text-muted font-weight-bold'><Countdown date={editThreshold} /></div>
: null}
setHasImgLink={setHasImgLink}
/>
{adv && <AdvPostForm edit={!!item} />}
<div className='mt-3'>
{item
? <EditFeeButton
paidSats={item.meSats} hadImgLink={item.paidImgLink} hasImgLink={hasImgLink}
paidSats={item.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
: <FeeButton
baseFee={1} hasImgLink={hasImgLink} parentId={null} text={buttonText}
baseFee={1} parentId={null} text={buttonText}
ChildButton={SubmitButton} variant='secondary'
/>}
</div>

View File

@ -110,8 +110,9 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
<Link href={`/items/${item.id}`} passHref>
<a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a>
</Link>
{me && !item.meSats && !item.position && !item.meDontLike && <DontLikeThis id={item.id} />}
{item.outlawed && <Link href='/outlawed'><a>{' '}<Badge className={styles.newComment} variant={null}>OUTLAWED</Badge></a></Link>}
{me && !item.meSats && !item.position && !item.meDontLike && !item.mine && <DontLikeThis id={item.id} />}
{(item.outlawed && <Link href='/outlawed'><a>{' '}<Badge className={styles.newComment} variant={null}>OUTLAWED</Badge></a></Link>) ||
(item.freebie && !item.mine && (me?.greeterMode) && <Link href='/freebie'><a>{' '}<Badge className={styles.newComment} variant={null}>FREEBIE</Badge></a></Link>)}
{item.prior &&
<>
<span> \ </span>

View File

@ -23,6 +23,7 @@ a.title:visited {
.newComment {
color: var(--theme-grey) !important;
background: var(--theme-clickToContextColor) !important;
vertical-align: middle;
}
.pin {

View File

@ -6,13 +6,11 @@ import Countdown from './countdown'
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
import { MAX_TITLE_LENGTH, MAX_POLL_CHOICE_LENGTH, MAX_POLL_NUM_CHOICES } from '../lib/constants'
import TextareaAutosize from 'react-textarea-autosize'
import { useState } from 'react'
import FeeButton, { EditFeeButton } from './fee-button'
export function PollForm ({ item, editThreshold }) {
const router = useRouter()
const client = useApolloClient()
const [hasImgLink, setHasImgLink] = useState()
const [upsertPoll] = useMutation(
gql`
@ -82,7 +80,6 @@ export function PollForm ({ item, editThreshold }) {
name='text'
as={TextareaAutosize}
minRows={2}
setHasImgLink={setHasImgLink}
/>
<VariableInput
label='choices'
@ -97,11 +94,11 @@ export function PollForm ({ item, editThreshold }) {
<div className='mt-3'>
{item
? <EditFeeButton
paidSats={item.meSats} hadImgLink={item.paidImgLink} hasImgLink={hasImgLink}
paidSats={item.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
: <FeeButton
baseFee={1} hasImgLink={hasImgLink} parentId={null} text='post'
baseFee={1} parentId={null} text='post'
ChildButton={SubmitButton} variant='secondary'
/>}
</div>

View File

@ -25,7 +25,6 @@ export function ReplyOnAnotherPage ({ parentId }) {
export default function Reply ({ item, onSuccess, replyOpen }) {
const [reply, setReply] = useState(replyOpen)
const me = useMe()
const [hasImgLink, setHasImgLink] = useState()
const parentId = item.id
useEffect(() => {
@ -104,7 +103,6 @@ export default function Reply ({ item, onSuccess, replyOpen }) {
}
resetForm({ text: '' })
setReply(replyOpen || false)
setHasImgLink(false)
}}
storageKeyPrefix={'reply-' + parentId}
>
@ -114,13 +112,12 @@ export default function Reply ({ item, onSuccess, replyOpen }) {
minRows={6}
autoFocus={!replyOpen}
required
setHasImgLink={setHasImgLink}
hint={me?.freeComments ? <span className='text-success'>{me.freeComments} free comments left</span> : null}
/>
{reply &&
<div className='mt-1'>
<FeeButton
baseFee={1} hasImgLink={hasImgLink} parentId={parentId} text='reply'
baseFee={1} parentId={parentId} text='reply'
ChildButton={SubmitButton} variant='secondary' alwaysShow
/>
</div>}

View File

@ -16,10 +16,10 @@ export const COMMENT_FIELDS = gql`
meSats
meDontLike
outlawed
freebie
path
commentSats
mine
paidImgLink
ncomments
root {
id

View File

@ -23,6 +23,7 @@ export const ITEM_FIELDS = gql`
meSats
meDontLike
outlawed
freebie
ncomments
commentSats
lastCommentAt
@ -38,7 +39,6 @@ export const ITEM_FIELDS = gql`
status
uploadId
mine
paidImgLink
root {
id
title
@ -95,6 +95,19 @@ export const BORDERLAND_ITEMS = gql`
}
}`
export const FREEBIE_ITEMS = gql`
${ITEM_FIELDS}
query freebieItems($cursor: String) {
freebieItems(cursor: $cursor) {
cursor
items {
...ItemFields
text
}
}
}`
export const POLL_FIELDS = gql`
fragment PollFields on Item {
poll {

View File

@ -26,6 +26,7 @@ export const ME = gql`
noteJobIndicator
hideInvoiceDesc
wildWestMode
greeterMode
lastCheckedJobs
}
}`
@ -52,6 +53,7 @@ export const ME_SSR = gql`
noteJobIndicator
hideInvoiceDesc
wildWestMode
greeterMode
lastCheckedJobs
}
}`
@ -68,6 +70,7 @@ export const SETTINGS_FIELDS = gql`
noteJobIndicator
hideInvoiceDesc
wildWestMode
greeterMode
authMethods {
lightning
email
@ -89,11 +92,13 @@ gql`
${SETTINGS_FIELDS}
mutation setSettings($tipDefault: Int!, $noteItemSats: Boolean!, $noteEarning: Boolean!,
$noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!, $wildWestMode: Boolean!) {
$noteInvites: Boolean!, $noteJobIndicator: Boolean!, $hideInvoiceDesc: Boolean!,
$wildWestMode: Boolean!, $greeterMode: Boolean!) {
setSettings(tipDefault: $tipDefault, noteItemSats: $noteItemSats,
noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, wildWestMode: $wildWestMode) {
noteJobIndicator: $noteJobIndicator, hideInvoiceDesc: $hideInvoiceDesc, wildWestMode: $wildWestMode,
greeterMode: $greeterMode) {
...SettingsFields
}
}

View File

@ -23,8 +23,6 @@ const BioSchema = Yup.object({
})
export function BioForm ({ handleSuccess, bio }) {
const [hasImgLink, setHasImgLink] = useState()
const [upsertBio] = useMutation(
gql`
${ITEM_FIELDS}
@ -70,16 +68,15 @@ export function BioForm ({ handleSuccess, bio }) {
name='bio'
as={TextareaAutosize}
minRows={6}
setHasImgLink={setHasImgLink}
/>
<div className='mt-3'>
{bio?.text
? <EditFeeButton
paidSats={bio?.meSats} hadImgLink={bio?.paidImgLink} hasImgLink={hasImgLink}
paidSats={bio?.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/>
: <FeeButton
baseFee={1} hasImgLink={hasImgLink} parentId={null} text='create'
baseFee={1} parentId={null} text='create'
ChildButton={SubmitButton} variant='secondary'
/>}
</div>

32
pages/freebie.js Normal file
View File

@ -0,0 +1,32 @@
import Layout from '../components/layout'
import { ItemsSkeleton } from '../components/items'
import { getGetServerSideProps } from '../api/ssrApollo'
import { FREEBIE_ITEMS } from '../fragments/items'
import { useQuery } from '@apollo/client'
import MixedItems from '../components/items-mixed'
export const getServerSideProps = getGetServerSideProps(FREEBIE_ITEMS)
export default function Index ({ data: { freebieItems: { items, cursor } } }) {
return (
<Layout>
<Items
items={items} cursor={cursor}
/>
</Layout>
)
}
function Items ({ rank, items, cursor }) {
const { data, fetchMore } = useQuery(FREEBIE_ITEMS)
if (!data && !items) {
return <ItemsSkeleton rank={rank} />
}
if (data) {
({ freebieItems: { items, cursor } } = data)
}
return <MixedItems items={items} cursor={cursor} rank={rank} fetchMore={fetchMore} />
}

View File

@ -62,7 +62,8 @@ export default function Settings ({ data: { settings } }) {
noteInvites: settings?.noteInvites,
noteJobIndicator: settings?.noteJobIndicator,
hideInvoiceDesc: settings?.hideInvoiceDesc,
wildWestMode: settings?.wildWestMode
wildWestMode: settings?.wildWestMode,
greeterMode: settings?.greeterMode
}}
schema={SettingsSchema}
onSubmit={async ({ tipDefault, ...values }) => {
@ -138,13 +139,28 @@ export default function Settings ({ data: { settings } }) {
<div className='d-flex align-items-center'>wild west mode
<Info>
<ul className='font-weight-bold'>
<li>Don't hide flagged content</li>
<li>Don't down rank flagged content</li>
<li>don't hide flagged content</li>
<li>don't down rank flagged content</li>
</ul>
</Info>
</div>
}
name='wildWestMode'
groupClassName='mb-0'
/>
<Checkbox
label={
<div className='d-flex align-items-center'>greeter mode
<Info>
<ul className='font-weight-bold'>
<li>see and screen free posts and comments</li>
<li>help onboard users to SN and Lightning</li>
<li>you might be subject to more spam</li>
</ul>
</Info>
</div>
}
name='greeterMode'
/>
<div className='d-flex'>
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton>

View File

@ -1 +1 @@
CREATE INDEX IF NOT EXISTS "item_gist_path_index" ON "Item" USING GIST ("path");
CREATE INDEX "item_gist_path_index" ON "Item" USING GIST ("path" gist_ltree_ops(siglen=2024));

View File

@ -0,0 +1,9 @@
-- AlterTable
ALTER TABLE "Item"
ADD COLUMN "bio" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "freebie" BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE "users" ADD COLUMN "greeterMode" BOOLEAN NOT NULL DEFAULT false,
ALTER COLUMN "freeComments" SET DEFAULT 5,
ALTER COLUMN "freePosts" SET DEFAULT 2;

View File

@ -0,0 +1,172 @@
DROP FUNCTION IF EXISTS create_bio(title TEXT, text TEXT, user_id INTEGER, has_img_link BOOLEAN);
-- when creating bio, set bio flag so they won't appear on first page
CREATE OR REPLACE FUNCTION create_bio(title TEXT, text TEXT, user_id INTEGER)
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
item "Item";
BEGIN
PERFORM ASSERT_SERIALIZED();
SELECT * INTO item FROM create_item(title, NULL, text, 0, NULL, user_id, NULL, '0');
UPDATE "Item" SET bio = true WHERE id = item.id;
UPDATE users SET "bioId" = item.id WHERE id = user_id;
RETURN item;
END;
$$;
DROP FUNCTION IF EXISTS create_item(
title TEXT, url TEXT, text TEXT, boost INTEGER,
parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
has_img_link BOOLEAN, spam_within INTERVAL);
-- when creating free item, set freebie flag so can be optionally viewed
CREATE OR REPLACE FUNCTION create_item(
title TEXT, url TEXT, text TEXT, boost INTEGER,
parent_id INTEGER, user_id INTEGER, fwd_user_id INTEGER,
spam_within INTERVAL)
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
user_msats INTEGER;
cost INTEGER;
free_posts INTEGER;
free_comments INTEGER;
freebie BOOLEAN;
item "Item";
med_votes FLOAT;
BEGIN
PERFORM ASSERT_SERIALIZED();
SELECT msats, "freePosts", "freeComments"
INTO user_msats, free_posts, free_comments
FROM users WHERE id = user_id;
cost := 1000 * POWER(10, item_spam(parent_id, user_id, spam_within));
freebie := (cost <= 1000) AND ((parent_id IS NULL AND free_posts > 0) OR (parent_id IS NOT NULL AND free_comments > 0));
IF NOT freebie AND cost > 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" = user_id;
-- 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
IF med_votes >= 0 THEN
med_votes := 0;
ELSE
med_votes := ABS(med_votes);
END IF;
INSERT INTO "Item" (title, url, text, "userId", "parentId", "fwdUserId", freebie, "weightedDownVotes", created_at, updated_at)
VALUES (title, url, text, user_id, parent_id, fwd_user_id, freebie, med_votes, now_utc(), now_utc()) RETURNING * INTO item;
IF freebie THEN
IF parent_id IS NULL THEN
UPDATE users SET "freePosts" = "freePosts" - 1 WHERE id = user_id;
ELSE
UPDATE users SET "freeComments" = "freeComments" - 1 WHERE id = user_id;
END IF;
ELSE
UPDATE users SET msats = msats - cost WHERE id = user_id;
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
VALUES (cost / 1000, item.id, user_id, 'VOTE', now_utc(), now_utc());
END IF;
IF boost > 0 THEN
PERFORM item_act(item.id, user_id, 'BOOST', boost);
END IF;
RETURN item;
END;
$$;
DROP FUNCTION IF EXISTS update_item(item_id INTEGER,
item_title TEXT, item_url TEXT, item_text TEXT, boost INTEGER,
fwd_user_id INTEGER, has_img_link BOOLEAN);
CREATE OR REPLACE FUNCTION update_item(item_id INTEGER,
item_title TEXT, item_url TEXT, item_text TEXT, boost INTEGER,
fwd_user_id INTEGER)
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
user_msats INTEGER;
item "Item";
BEGIN
PERFORM ASSERT_SERIALIZED();
UPDATE "Item" set title = item_title, url = item_url, text = item_text, "fwdUserId" = fwd_user_id
WHERE id = item_id
RETURNING * INTO item;
IF boost > 0 THEN
PERFORM item_act(item.id, item."userId", 'BOOST', boost);
END IF;
RETURN item;
END;
$$;
DROP FUNCTION IF EXISTS create_poll(
title TEXT, text TEXT, poll_cost INTEGER, boost INTEGER, user_id INTEGER,
options TEXT[], fwd_user_id INTEGER, has_img_link BOOLEAN, spam_within INTERVAL);
CREATE OR REPLACE FUNCTION create_poll(
title TEXT, text TEXT, poll_cost INTEGER, boost INTEGER, user_id INTEGER,
options TEXT[], fwd_user_id INTEGER, spam_within INTERVAL)
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
item "Item";
option TEXT;
BEGIN
PERFORM ASSERT_SERIALIZED();
item := create_item(title, null, text, boost, null, user_id, fwd_user_id, spam_within);
UPDATE "Item" set "pollCost" = poll_cost where id = item.id;
FOREACH option IN ARRAY options LOOP
INSERT INTO "PollOption" (created_at, updated_at, "itemId", "option") values (now_utc(), now_utc(), item.id, option);
END LOOP;
RETURN item;
END;
$$;
DROP FUNCTION IF EXISTS update_poll(
id INTEGER, title TEXT, text TEXT, boost INTEGER,
options TEXT[], fwd_user_id INTEGER, has_img_link BOOLEAN);
CREATE OR REPLACE FUNCTION update_poll(
id INTEGER, title TEXT, text TEXT, boost INTEGER,
options TEXT[], fwd_user_id INTEGER)
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
item "Item";
option TEXT;
BEGIN
PERFORM ASSERT_SERIALIZED();
item := update_item(id, title, null, text, boost, fwd_user_id);
FOREACH option IN ARRAY options LOOP
INSERT INTO "PollOption" (created_at, updated_at, "itemId", "option") values (now_utc(), now_utc(), item.id, option);
END LOOP;
RETURN item;
END;
$$;

View File

@ -32,8 +32,8 @@ model User {
bioId Int?
msats Int @default(0)
stackedMsats Int @default(0)
freeComments Int @default(0)
freePosts Int @default(0)
freeComments Int @default(5)
freePosts Int @default(2)
checkedNotesAt DateTime?
tipDefault Int @default(10)
pubkey String? @unique
@ -61,6 +61,7 @@ model User {
// content settings
wildWestMode Boolean @default(false)
greeterMode Boolean @default(false)
Earn Earn[]
Upload Upload[] @relation(name: "Uploads")
@ -185,6 +186,10 @@ model Item {
upload Upload?
paidImgLink Boolean @default(false)
// is free post or bio
freebie Boolean @default(false)
bio Boolean @default(false)
// denormalized self stats
weightedVotes Float @default(0)
weightedDownVotes Float @default(0)