diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 78a09ade..5afa557b 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -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 ` diff --git a/api/resolvers/user.js b/api/resolvers/user.js index fb3277b5..d4d81ac8 100644 --- a/api/resolvers/user.js +++ b/api/resolvers/user.js @@ -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) } diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 6b3bf2bd..2d7e0217 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -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!]! diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js index 411fb9b6..8e580219 100644 --- a/api/typeDefs/user.js +++ b/api/typeDefs/user.js @@ -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! } diff --git a/components/comment-edit.js b/components/comment-edit.js index 971fc609..3d78b375 100644 --- a/components/comment-edit.js +++ b/components/comment-edit.js @@ -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 /> diff --git a/components/comment.js b/components/comment.js index 5768fc17..157ba5e8 100644 --- a/components/comment.js +++ b/components/comment.js @@ -133,8 +133,9 @@ export default function Comment ({ {timeSince(new Date(item.createdAt))} {includeParent && } - {me && !item.meSats && !item.meDontLike && } - {item.outlawed && {' '}OUTLAWED} + {me && !item.meSats && !item.meDontLike && !item.mine && } + {(item.outlawed && {' '}OUTLAWED) || + (item.freebie && !item.mine && (me?.greeterMode) && {' '}FREEBIE)} {canEdit && <> \ diff --git a/components/discussion-form.js b/components/discussion-form.js index be90ebcf..611d97d3 100644 --- a/components/discussion-form.js +++ b/components/discussion-form.js @@ -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 ?
: null} - setHasImgLink={setHasImgLink} /> {adv && }
{item ? : }
diff --git a/components/item.js b/components/item.js index cda815bc..651f8c76 100644 --- a/components/item.js +++ b/components/item.js @@ -110,8 +110,9 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) { {timeSince(new Date(item.createdAt))} - {me && !item.meSats && !item.position && !item.meDontLike && } - {item.outlawed && {' '}OUTLAWED} + {me && !item.meSats && !item.position && !item.meDontLike && !item.mine && } + {(item.outlawed && {' '}OUTLAWED) || + (item.freebie && !item.mine && (me?.greeterMode) && {' '}FREEBIE)} {item.prior && <> \ diff --git a/components/item.module.css b/components/item.module.css index eb9bd351..19bc09c1 100644 --- a/components/item.module.css +++ b/components/item.module.css @@ -23,6 +23,7 @@ a.title:visited { .newComment { color: var(--theme-grey) !important; background: var(--theme-clickToContextColor) !important; + vertical-align: middle; } .pin { diff --git a/components/poll-form.js b/components/poll-form.js index 597f7bf5..664ea441 100644 --- a/components/poll-form.js +++ b/components/poll-form.js @@ -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} /> {item ? : } diff --git a/components/reply.js b/components/reply.js index 44fade81..bf863d43 100644 --- a/components/reply.js +++ b/components/reply.js @@ -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 ? {me.freeComments} free comments left : null} /> {reply &&
} diff --git a/fragments/comments.js b/fragments/comments.js index 5e188576..f54705d2 100644 --- a/fragments/comments.js +++ b/fragments/comments.js @@ -16,10 +16,10 @@ export const COMMENT_FIELDS = gql` meSats meDontLike outlawed + freebie path commentSats mine - paidImgLink ncomments root { id diff --git a/fragments/items.js b/fragments/items.js index acdecb8d..763a355e 100644 --- a/fragments/items.js +++ b/fragments/items.js @@ -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 { diff --git a/fragments/users.js b/fragments/users.js index 8ccbe45c..7cd460ad 100644 --- a/fragments/users.js +++ b/fragments/users.js @@ -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 } } diff --git a/pages/[name]/index.js b/pages/[name]/index.js index 5788b0e5..6aff4a24 100644 --- a/pages/[name]/index.js +++ b/pages/[name]/index.js @@ -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} />
{bio?.text ? : }
diff --git a/pages/freebie.js b/pages/freebie.js new file mode 100644 index 00000000..ad5d49c8 --- /dev/null +++ b/pages/freebie.js @@ -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 ( + + + + ) +} + +function Items ({ rank, items, cursor }) { + const { data, fetchMore } = useQuery(FREEBIE_ITEMS) + + if (!data && !items) { + return + } + + if (data) { + ({ freebieItems: { items, cursor } } = data) + } + + return +} diff --git a/pages/settings.js b/pages/settings.js index cc7a7789..51673427 100644 --- a/pages/settings.js +++ b/pages/settings.js @@ -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 } }) {
wild west mode
    -
  • Don't hide flagged content
  • -
  • Don't down rank flagged content
  • +
  • don't hide flagged content
  • +
  • don't down rank flagged content
} name='wildWestMode' + groupClassName='mb-0' + /> + greeter mode + +
    +
  • see and screen free posts and comments
  • +
  • help onboard users to SN and Lightning
  • +
  • you might be subject to more spam
  • +
+
+ + } + name='greeterMode' />
save diff --git a/prisma/migrations/20220412190704_item_path_index/migration.sql b/prisma/migrations/20220412190704_item_path_index/migration.sql index f701cd03..2ec14089 100644 --- a/prisma/migrations/20220412190704_item_path_index/migration.sql +++ b/prisma/migrations/20220412190704_item_path_index/migration.sql @@ -1 +1 @@ -CREATE INDEX IF NOT EXISTS "item_gist_path_index" ON "Item" USING GIST ("path"); \ No newline at end of file +CREATE INDEX "item_gist_path_index" ON "Item" USING GIST ("path" gist_ltree_ops(siglen=2024)); \ No newline at end of file diff --git a/prisma/migrations/20220926201629_freebies/migration.sql b/prisma/migrations/20220926201629_freebies/migration.sql new file mode 100644 index 00000000..7e8139aa --- /dev/null +++ b/prisma/migrations/20220926201629_freebies/migration.sql @@ -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; \ No newline at end of file diff --git a/prisma/migrations/20220926204325_item_bio/migration.sql b/prisma/migrations/20220926204325_item_bio/migration.sql new file mode 100644 index 00000000..0dac0f3d --- /dev/null +++ b/prisma/migrations/20220926204325_item_bio/migration.sql @@ -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; +$$; \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 64e4ed54..7e9df16d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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)