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)