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, BOOST_MIN, ITEM_SPAM_INTERVAL, MAX_POLL_NUM_CHOICES,
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD, DONT_LIKE_THIS_COST
} from '../../lib/constants' } from '../../lib/constants'
import { mdHas } from '../../lib/md'
async function comments (me, models, id, sort) { async function comments (me, models, id, sort) {
let orderBy let orderBy
@ -85,15 +84,28 @@ export async function orderByNumerator (me, models) {
} }
export async function filterClause (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) { if (me) {
const user = await models.user.findUnique({ where: { id: me.id } }) const user = await models.user.findUnique({ where: { id: me.id } })
// wild west mode has everything
if (user.wildWestMode) { if (user.wildWestMode) {
return '' 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 // 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) { if (me) {
clause += ` OR "Item"."userId" = ${me.id}` clause += ` OR "Item"."userId" = ${me.id}`
} }
@ -215,7 +227,7 @@ export default {
${SELECT} ${SELECT}
FROM "Item" FROM "Item"
WHERE "parentId" IS NULL AND "Item".created_at <= $1 AND "Item".created_at > $3 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)} ${subClause(4)}
${await filterClause(me, models)} ${await filterClause(me, models)}
${await newTimedOrderByWeightedSats(me, models, 1)} ${await newTimedOrderByWeightedSats(me, models, 1)}
@ -228,7 +240,7 @@ export default {
${SELECT} ${SELECT}
FROM "Item" FROM "Item"
WHERE "parentId" IS NULL AND "Item".created_at <= $1 WHERE "parentId" IS NULL AND "Item".created_at <= $1
AND "pinId" IS NULL AND "pinId" IS NULL AND NOT bio
${subClause(3)} ${subClause(3)}
${await filterClause(me, models)} ${await filterClause(me, models)}
${await newTimedOrderByWeightedSats(me, models, 1)} ${await newTimedOrderByWeightedSats(me, models, 1)}
@ -312,6 +324,21 @@ export default {
items 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 }) => { moreFlatComments: async (parent, { cursor, name, sort, within }, { me, models }) => {
const decodedCursor = decodeCursor(cursor) const decodedCursor = decodeCursor(cursor)
@ -574,8 +601,6 @@ export default {
} }
} }
const hasImgLink = !!(text && mdHas(text, ['link', 'image']))
if (id) { if (id) {
const optionCount = await models.pollOption.count({ const optionCount = await models.pollOption.count({
where: { where: {
@ -588,8 +613,8 @@ export default {
} }
const [item] = await serialize(models, const [item] = await serialize(models,
models.$queryRaw(`${SELECT} FROM update_poll($1, $2, $3, $4, $5, $6, $7) AS "Item"`, 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), hasImgLink)) Number(id), title, text, Number(boost || 0), options, Number(fwdUser?.id)))
return item return item
} else { } else {
@ -598,8 +623,8 @@ export default {
} }
const [item] = await serialize(models, const [item] = await serialize(models,
models.$queryRaw(`${SELECT} FROM create_poll($1, $2, $3, $4, $5, $6, $7, $8, '${ITEM_SPAM_INTERVAL}') AS "Item"`, 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), hasImgLink)) title, text, 1, Number(boost || 0), Number(me.id), options, Number(fwdUser?.id)))
await createMentions(item, models) 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, const [item] = await serialize(models,
models.$queryRaw( models.$queryRaw(
`${SELECT} FROM update_item($1, $2, $3, $4, $5, $6, $7) AS "Item"`, `${SELECT} FROM update_item($1, $2, $3, $4, $5, $6) AS "Item"`,
Number(id), title, url, text, Number(boost || 0), Number(fwdUser?.id), hasImgLink)) Number(id), title, url, text, Number(boost || 0), Number(fwdUser?.id)))
await createMentions(item, models) 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, const [item] = await serialize(models,
models.$queryRaw( 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), title, url, text, Number(boost || 0), Number(parentId), Number(me.id),
Number(fwdUser?.id), hasImgLink)) Number(fwdUser?.id)))
await createMentions(item, models) 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, `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".text, "Item".url, "Item"."userId", "Item"."fwdUserId", "Item"."parentId", "Item"."pinId", "Item"."maxBid",
"Item".company, "Item".location, "Item".remote, "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".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) { async function newTimedOrderByWeightedSats (me, models, num) {
return ` return `

View File

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

View File

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

View File

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

View File

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

View File

@ -133,8 +133,9 @@ export default function Comment ({
<a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a> <a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a>
</Link> </Link>
{includeParent && <Parent item={item} rootText={rootText} />} {includeParent && <Parent item={item} rootText={rootText} />}
{me && !item.meSats && !item.meDontLike && <DontLikeThis id={item.id} />} {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.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 && {canEdit &&
<> <>
<span> \ </span> <span> \ </span>

View File

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

View File

@ -110,8 +110,9 @@ export default function Item ({ item, rank, showFwdUser, toc, children }) {
<Link href={`/items/${item.id}`} passHref> <Link href={`/items/${item.id}`} passHref>
<a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a> <a title={item.createdAt} className='text-reset'>{timeSince(new Date(item.createdAt))}</a>
</Link> </Link>
{me && !item.meSats && !item.position && !item.meDontLike && <DontLikeThis id={item.id} />} {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.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 && {item.prior &&
<> <>
<span> \ </span> <span> \ </span>

View File

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

View File

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

View File

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

View File

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

View File

@ -23,6 +23,7 @@ export const ITEM_FIELDS = gql`
meSats meSats
meDontLike meDontLike
outlawed outlawed
freebie
ncomments ncomments
commentSats commentSats
lastCommentAt lastCommentAt
@ -38,7 +39,6 @@ export const ITEM_FIELDS = gql`
status status
uploadId uploadId
mine mine
paidImgLink
root { root {
id id
title 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` export const POLL_FIELDS = gql`
fragment PollFields on Item { fragment PollFields on Item {
poll { poll {

View File

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

View File

@ -23,8 +23,6 @@ const BioSchema = Yup.object({
}) })
export function BioForm ({ handleSuccess, bio }) { export function BioForm ({ handleSuccess, bio }) {
const [hasImgLink, setHasImgLink] = useState()
const [upsertBio] = useMutation( const [upsertBio] = useMutation(
gql` gql`
${ITEM_FIELDS} ${ITEM_FIELDS}
@ -70,16 +68,15 @@ export function BioForm ({ handleSuccess, bio }) {
name='bio' name='bio'
as={TextareaAutosize} as={TextareaAutosize}
minRows={6} minRows={6}
setHasImgLink={setHasImgLink}
/> />
<div className='mt-3'> <div className='mt-3'>
{bio?.text {bio?.text
? <EditFeeButton ? <EditFeeButton
paidSats={bio?.meSats} hadImgLink={bio?.paidImgLink} hasImgLink={hasImgLink} paidSats={bio?.meSats}
parentId={null} text='save' ChildButton={SubmitButton} variant='secondary' parentId={null} text='save' ChildButton={SubmitButton} variant='secondary'
/> />
: <FeeButton : <FeeButton
baseFee={1} hasImgLink={hasImgLink} parentId={null} text='create' baseFee={1} parentId={null} text='create'
ChildButton={SubmitButton} variant='secondary' ChildButton={SubmitButton} variant='secondary'
/>} />}
</div> </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, noteInvites: settings?.noteInvites,
noteJobIndicator: settings?.noteJobIndicator, noteJobIndicator: settings?.noteJobIndicator,
hideInvoiceDesc: settings?.hideInvoiceDesc, hideInvoiceDesc: settings?.hideInvoiceDesc,
wildWestMode: settings?.wildWestMode wildWestMode: settings?.wildWestMode,
greeterMode: settings?.greeterMode
}} }}
schema={SettingsSchema} schema={SettingsSchema}
onSubmit={async ({ tipDefault, ...values }) => { onSubmit={async ({ tipDefault, ...values }) => {
@ -138,13 +139,28 @@ export default function Settings ({ data: { settings } }) {
<div className='d-flex align-items-center'>wild west mode <div className='d-flex align-items-center'>wild west mode
<Info> <Info>
<ul className='font-weight-bold'> <ul className='font-weight-bold'>
<li>Don't hide flagged content</li> <li>don't hide flagged content</li>
<li>Don't down rank flagged content</li> <li>don't down rank flagged content</li>
</ul> </ul>
</Info> </Info>
</div> </div>
} }
name='wildWestMode' 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'> <div className='d-flex'>
<SubmitButton variant='info' className='ml-auto mt-1 px-4'>save</SubmitButton> <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? bioId Int?
msats Int @default(0) msats Int @default(0)
stackedMsats Int @default(0) stackedMsats Int @default(0)
freeComments Int @default(0) freeComments Int @default(5)
freePosts Int @default(0) freePosts Int @default(2)
checkedNotesAt DateTime? checkedNotesAt DateTime?
tipDefault Int @default(10) tipDefault Int @default(10)
pubkey String? @unique pubkey String? @unique
@ -61,6 +61,7 @@ model User {
// content settings // content settings
wildWestMode Boolean @default(false) wildWestMode Boolean @default(false)
greeterMode Boolean @default(false)
Earn Earn[] Earn Earn[]
Upload Upload[] @relation(name: "Uploads") Upload Upload[] @relation(name: "Uploads")
@ -185,6 +186,10 @@ model Item {
upload Upload? upload Upload?
paidImgLink Boolean @default(false) paidImgLink Boolean @default(false)
// is free post or bio
freebie Boolean @default(false)
bio Boolean @default(false)
// denormalized self stats // denormalized self stats
weightedVotes Float @default(0) weightedVotes Float @default(0)
weightedDownVotes Float @default(0) weightedDownVotes Float @default(0)