raise boost minimum to 25k, enhance editing boost, JIT fund editing costs
This commit is contained in:
parent
374cc26224
commit
370e3c1c48
|
@ -12,7 +12,7 @@ import {
|
|||
import { msatsToSats, numWithUnits } from '../../lib/format'
|
||||
import { parse } from 'tldts'
|
||||
import uu from 'url-unshort'
|
||||
import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
|
||||
import { advSchema, amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
|
||||
import { sendUserNotification } from '../webPush'
|
||||
import { proxyImages } from './imgproxy'
|
||||
import { defaultCommentSort } from '../../lib/item'
|
||||
|
@ -624,34 +624,34 @@ export default {
|
|||
return await models.item.update({ where: { id: Number(id) }, data })
|
||||
},
|
||||
upsertLink: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(linkSchema, item, models, me)
|
||||
await ssValidate(linkSchema, item, { models, me })
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
return await updateItem(parent, { id, ...item }, { me, models, lnd, hash, hmac })
|
||||
} else {
|
||||
return await createItem(parent, item, { me, models, lnd, hash, hmac })
|
||||
}
|
||||
},
|
||||
upsertDiscussion: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(discussionSchema, item, models, me)
|
||||
await ssValidate(discussionSchema, item, { models, me })
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
return await updateItem(parent, { id, ...item }, { me, models, lnd, hash, hmac })
|
||||
} else {
|
||||
return await createItem(parent, item, { me, models, lnd, hash, hmac })
|
||||
}
|
||||
},
|
||||
upsertBounty: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
|
||||
await ssValidate(bountySchema, item, models, me)
|
||||
await ssValidate(bountySchema, item, { models, me })
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
return await updateItem(parent, { id, ...item }, { me, models, lnd, hash, hmac })
|
||||
} else {
|
||||
return await createItem(parent, item, { me, models, lnd, hash, hmac })
|
||||
}
|
||||
},
|
||||
upsertPoll: async (parent, { id, hash, hmac, ...item }, { me, models, lnd }) => {
|
||||
const optionCount = id
|
||||
const numExistingChoices = id
|
||||
? await models.pollOption.count({
|
||||
where: {
|
||||
itemId: Number(id)
|
||||
|
@ -659,10 +659,10 @@ export default {
|
|||
})
|
||||
: 0
|
||||
|
||||
await ssValidate(pollSchema, item, models, me, optionCount)
|
||||
await ssValidate(pollSchema, item, { models, me, numExistingChoices })
|
||||
|
||||
if (id) {
|
||||
return await updateItem(parent, { id, ...item }, { me, models })
|
||||
return await updateItem(parent, { id, ...item }, { me, models, lnd, hash, hmac })
|
||||
} else {
|
||||
item.pollCost = item.pollCost || POLL_COST
|
||||
return await createItem(parent, item, { me, models, lnd, hash, hmac })
|
||||
|
@ -674,7 +674,7 @@ export default {
|
|||
}
|
||||
|
||||
item.location = item.location?.toLowerCase() === 'remote' ? undefined : item.location
|
||||
await ssValidate(jobSchema, item, models)
|
||||
await ssValidate(jobSchema, item, { models })
|
||||
if (item.logo !== undefined) {
|
||||
item.uploadId = item.logo
|
||||
delete item.logo
|
||||
|
@ -1119,13 +1119,15 @@ export const createMentions = async (item, models) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models }) => {
|
||||
export const updateItem = async (parent, { sub: subName, forward, options, ...item }, { me, models, lnd, hash, hmac }) => {
|
||||
// update iff this item belongs to me
|
||||
const old = await models.item.findUnique({ where: { id: Number(item.id) } })
|
||||
if (Number(old.userId) !== Number(me?.id)) {
|
||||
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
|
||||
await ssValidate(advSchema, { boost: item.boost }, { models, me, existingBoost: old.boost })
|
||||
|
||||
// if it's not the FAQ, not their bio, and older than 10 minutes
|
||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||
if (![349, 76894, 78763, 81862].includes(old.id) && user.bioId !== old.id &&
|
||||
|
@ -1141,18 +1143,43 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
|
|||
item.url = removeTracking(item.url)
|
||||
item.url = await proxyImages(item.url)
|
||||
}
|
||||
// only update item with the boost delta ... this is a bit of hack given the way
|
||||
// boost used to work
|
||||
if (item.boost > 0 && old.boost > 0) {
|
||||
// only update the boost if it is higher than the old boost
|
||||
if (item.boost > old.boost) {
|
||||
item.boost = item.boost - old.boost
|
||||
} else {
|
||||
delete item.boost
|
||||
}
|
||||
}
|
||||
|
||||
item = { subName, userId: me.id, ...item }
|
||||
const fwdUsers = await getForwardUsers(models, forward)
|
||||
|
||||
const [rItem] = await serialize(models,
|
||||
let invoice
|
||||
if (hash) {
|
||||
invoice = await checkInvoice(models, hash, hmac)
|
||||
}
|
||||
|
||||
const trx = [
|
||||
models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB) AS "Item"`,
|
||||
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)))
|
||||
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options))
|
||||
]
|
||||
if (invoice) {
|
||||
trx.unshift(models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`)
|
||||
trx.push(models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } }))
|
||||
}
|
||||
|
||||
const query = await serialize(models, ...trx)
|
||||
const rItem = trx.length > 1 ? query[1][0] : query[0]
|
||||
|
||||
if (invoice?.isHeld) await settleHodlInvoice({ secret: invoice.preimage, lnd })
|
||||
|
||||
await createMentions(rItem, models)
|
||||
|
||||
item.comments = []
|
||||
return item
|
||||
rItem.comments = []
|
||||
return rItem
|
||||
}
|
||||
|
||||
export const createItem = async (parent, { forward, options, ...item }, { me, models, lnd, hash, hmac }) => {
|
||||
|
|
|
@ -484,7 +484,7 @@ export default {
|
|||
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
|
||||
}
|
||||
|
||||
await ssValidate(userSchema, data, models)
|
||||
await ssValidate(userSchema, data, { models })
|
||||
|
||||
try {
|
||||
await models.user.update({ where: { id: me.id }, data })
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
import AccordianItem from './accordian-item'
|
||||
import { Input, InputUserSuggest, VariableInput } from './form'
|
||||
import InputGroup from 'react-bootstrap/InputGroup'
|
||||
import { BOOST_MIN, MAX_FORWARDS } from '../lib/constants'
|
||||
import { BOOST_MIN, BOOST_MULT, MAX_FORWARDS } from '../lib/constants'
|
||||
import Info from './info'
|
||||
import { numWithUnits } from '../lib/format'
|
||||
import styles from './adv-post-form.module.css'
|
||||
|
||||
const EMPTY_FORWARD = { nym: '', pct: '' }
|
||||
|
||||
export function AdvPostInitial ({ forward }) {
|
||||
export function AdvPostInitial ({ forward, boost }) {
|
||||
return {
|
||||
boost: '',
|
||||
boost: boost || '',
|
||||
forward: forward?.length ? forward : [EMPTY_FORWARD]
|
||||
}
|
||||
}
|
||||
|
||||
export default function AdvPostForm ({ edit }) {
|
||||
export default function AdvPostForm () {
|
||||
return (
|
||||
<AccordianItem
|
||||
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>options</div>}
|
||||
|
@ -23,17 +23,17 @@ export default function AdvPostForm ({ edit }) {
|
|||
<>
|
||||
<Input
|
||||
label={
|
||||
<div className='d-flex align-items-center'>{edit ? 'add boost' : 'boost'}
|
||||
<div className='d-flex align-items-center'>boost
|
||||
<Info>
|
||||
<ol className='fw-bold'>
|
||||
<li>Boost ranks posts higher temporarily based on the amount</li>
|
||||
<li>The minimum boost is {numWithUnits(BOOST_MIN, { abbreviate: false })}</li>
|
||||
<li>Each {numWithUnits(BOOST_MIN, { abbreviate: false })} of boost is equivalent to one trusted upvote
|
||||
<li>Each {numWithUnits(BOOST_MULT, { abbreviate: false })} of boost is equivalent to one trusted upvote
|
||||
<ul>
|
||||
<li>e.g. {numWithUnits(BOOST_MIN * 2, { abbreviate: false })} is like 2 votes</li>
|
||||
<li>e.g. {numWithUnits(BOOST_MULT * 5, { abbreviate: false })} is like 5 votes</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>The decay of boost "votes" increases at 2x the rate of organic votes
|
||||
<li>The decay of boost "votes" increases at 1.25x the rate of organic votes
|
||||
<ul>
|
||||
<li>i.e. boost votes fall out of ranking faster</li>
|
||||
</ul>
|
||||
|
|
|
@ -27,7 +27,7 @@ export function BountyForm ({
|
|||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const me = useMe()
|
||||
const schema = bountySchema(client, me)
|
||||
const schema = bountySchema({ client, me, existingBoost: item?.boost })
|
||||
const [upsertBounty] = useMutation(
|
||||
gql`
|
||||
mutation upsertBounty(
|
||||
|
@ -89,7 +89,7 @@ export function BountyForm ({
|
|||
title: item?.title || '',
|
||||
text: item?.text || '',
|
||||
bounty: item?.bounty || 1000,
|
||||
...AdvPostInitial({ forward: normalizeForwards(item?.forwards) }),
|
||||
...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }),
|
||||
...SubSelectInitial({ sub: item?.subName || sub?.name })
|
||||
}}
|
||||
schema={schema}
|
||||
|
|
|
@ -25,7 +25,7 @@ export function DiscussionForm ({
|
|||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const me = useMe()
|
||||
const schema = discussionSchema(client, me)
|
||||
const schema = discussionSchema({ client, me, existingBoost: item?.boost })
|
||||
// if Web Share Target API was used
|
||||
const shareTitle = router.query.title
|
||||
|
||||
|
@ -81,7 +81,7 @@ export function DiscussionForm ({
|
|||
initial={{
|
||||
title: item?.title || shareTitle || '',
|
||||
text: item?.text || '',
|
||||
...AdvPostInitial({ forward: normalizeForwards(item?.forwards) }),
|
||||
...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }),
|
||||
...SubSelectInitial({ sub: item?.subName || sub?.name })
|
||||
}}
|
||||
schema={schema}
|
||||
|
|
|
@ -137,7 +137,7 @@ function EditReceipt ({ cost, paidSats, addImgLink, boost, parentId }) {
|
|||
|
||||
export function EditFeeButton ({ paidSats, hadImgLink, hasImgLink, ChildButton, variant, text, alwaysShow, parentId }) {
|
||||
const formik = useFormikContext()
|
||||
const boost = formik?.values?.boost || 0
|
||||
const boost = (formik?.values?.boost || 0) - (formik?.initialValues?.boost || 0)
|
||||
const addImgLink = hasImgLink && !hadImgLink
|
||||
const cost = (addImgLink ? paidSats * 9 : 0) + Number(boost)
|
||||
|
||||
|
@ -148,7 +148,7 @@ export function EditFeeButton ({ paidSats, hadImgLink, hasImgLink, ChildButton,
|
|||
const show = alwaysShow || !formik?.isSubmitting
|
||||
return (
|
||||
<div className='d-flex align-items-center'>
|
||||
<ActionTooltip overlayText={numWithUnits(cost, { abbreviate: false })}>
|
||||
<ActionTooltip overlayText={numWithUnits(cost >= 0 ? cost : 0, { abbreviate: false })}>
|
||||
<ChildButton variant={variant}>{text}{cost > 0 && show && <small> {numWithUnits(cost, { abbreviate: false })}</small>}</ChildButton>
|
||||
</ActionTooltip>
|
||||
{cost > 0 && show &&
|
||||
|
|
|
@ -22,7 +22,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const me = useMe()
|
||||
const schema = linkSchema(client, me)
|
||||
const schema = linkSchema({ client, me, existingBoost: item?.boost })
|
||||
// if Web Share Target API was used
|
||||
const shareUrl = router.query.url
|
||||
const shareTitle = router.query.title
|
||||
|
@ -123,7 +123,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||
initial={{
|
||||
title: item?.title || shareTitle || '',
|
||||
url: item?.url || shareUrl || '',
|
||||
...AdvPostInitial({ forward: normalizeForwards(item?.forwards) }),
|
||||
...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }),
|
||||
...SubSelectInitial({ sub: item?.subName || sub?.name })
|
||||
}}
|
||||
schema={schema}
|
||||
|
|
|
@ -18,7 +18,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const me = useMe()
|
||||
const schema = pollSchema(client, me)
|
||||
const schema = pollSchema({ client, me, existingBoost: item?.boost })
|
||||
|
||||
const [upsertPoll] = useMutation(
|
||||
gql`
|
||||
|
@ -65,7 +65,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||
title: item?.title || '',
|
||||
text: item?.text || '',
|
||||
options: initialOptions || ['', ''],
|
||||
...AdvPostInitial({ forward: normalizeForwards(item?.forwards) }),
|
||||
...AdvPostInitial({ forward: normalizeForwards(item?.forwards), boost: item?.boost }),
|
||||
...SubSelectInitial({ sub: item?.subName || sub?.name })
|
||||
}}
|
||||
schema={schema}
|
||||
|
|
|
@ -105,7 +105,7 @@ function NymEdit ({ user, setEditting }) {
|
|||
}
|
||||
})
|
||||
const client = useApolloClient()
|
||||
const schema = userSchema(client)
|
||||
const schema = userSchema({ client })
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
|
|
@ -4,7 +4,8 @@ export const SUBS = ['bitcoin', 'nostr', 'tech', 'meta', 'jobs']
|
|||
export const SUBS_NO_JOBS = SUBS.filter(s => s !== 'jobs')
|
||||
|
||||
export const NOFOLLOW_LIMIT = 100
|
||||
export const BOOST_MIN = 5000
|
||||
export const BOOST_MULT = 5000
|
||||
export const BOOST_MIN = BOOST_MULT * 5
|
||||
export const UPLOAD_SIZE_MAX = 2 * 1024 * 1024
|
||||
export const IMAGE_PIXELS_MAX = 35000000
|
||||
export const UPLOAD_TYPES_ALLOW = [
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { string, ValidationError, number, object, array, addMethod, boolean } from 'yup'
|
||||
import { BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES, MIN_POLL_NUM_CHOICES, SUBS_NO_JOBS, MAX_FORWARDS } from './constants'
|
||||
import { BOOST_MIN, MAX_POLL_CHOICE_LENGTH, MAX_TITLE_LENGTH, MAX_POLL_NUM_CHOICES, MIN_POLL_NUM_CHOICES, SUBS_NO_JOBS, MAX_FORWARDS, BOOST_MULT } from './constants'
|
||||
import { NAME_QUERY } from '../fragments/users'
|
||||
import { URL_REGEXP, WS_REGEXP } from './url'
|
||||
import { SUPPORTED_CURRENCIES } from './currency'
|
||||
import { NOSTR_MAX_RELAY_NUM, NOSTR_PUBKEY_BECH32, NOSTR_PUBKEY_HEX } from './nostr'
|
||||
|
||||
export async function ssValidate (schema, data, ...args) {
|
||||
export async function ssValidate (schema, data, args) {
|
||||
try {
|
||||
if (typeof schema === 'function') {
|
||||
await schema(...args).validate(data)
|
||||
await schema(args).validate(data)
|
||||
} else {
|
||||
await schema.validate(data)
|
||||
}
|
||||
|
@ -43,28 +43,29 @@ const titleValidator = string().required('required').trim().max(
|
|||
|
||||
const intValidator = number().typeError('must be a number').integer('must be whole')
|
||||
|
||||
async function usernameExists (client, name) {
|
||||
if (!client) {
|
||||
async function usernameExists (name, { client, models }) {
|
||||
if (!client && !models) {
|
||||
throw new Error('cannot check for user')
|
||||
}
|
||||
// apollo client
|
||||
if (client.query) {
|
||||
if (client) {
|
||||
const { data } = await client.query({ query: NAME_QUERY, variables: { name } })
|
||||
return !data.nameAvailable
|
||||
}
|
||||
|
||||
// prisma client
|
||||
const user = await client.user.findUnique({ where: { name } })
|
||||
const user = await models.user.findUnique({ where: { name } })
|
||||
return !!user
|
||||
}
|
||||
|
||||
export function advPostSchemaMembers (client, me) {
|
||||
export function advPostSchemaMembers ({ me, existingBoost = 0, ...args }) {
|
||||
const boostMin = existingBoost || BOOST_MIN
|
||||
return {
|
||||
boost: intValidator
|
||||
.min(BOOST_MIN, `must be blank or at least ${BOOST_MIN}`).test({
|
||||
.min(boostMin, `must be ${existingBoost ? '' : 'blank or '}at least ${boostMin}`).test({
|
||||
name: 'boost',
|
||||
test: async boost => !boost || boost % BOOST_MIN === 0,
|
||||
message: `must be divisble be ${BOOST_MIN}`
|
||||
test: async boost => (!existingBoost && !boost) || boost % BOOST_MULT === 0,
|
||||
message: `must be divisble be ${BOOST_MULT}`
|
||||
}),
|
||||
forward: array()
|
||||
.max(MAX_FORWARDS, `you can only configure ${MAX_FORWARDS} forward recipients`)
|
||||
|
@ -74,7 +75,7 @@ export function advPostSchemaMembers (client, me) {
|
|||
name: 'nym',
|
||||
test: async name => {
|
||||
if (!name || !name.length) return false
|
||||
return await usernameExists(client, name)
|
||||
return await usernameExists(name, args)
|
||||
},
|
||||
message: 'stacker does not exist'
|
||||
})
|
||||
|
@ -101,41 +102,47 @@ export function advPostSchemaMembers (client, me) {
|
|||
}
|
||||
}
|
||||
|
||||
export function subSelectSchemaMembers (client) {
|
||||
export function subSelectSchemaMembers () {
|
||||
return {
|
||||
sub: string().required('required').oneOf(SUBS_NO_JOBS, 'required')
|
||||
}
|
||||
}
|
||||
// for testing advPostSchemaMembers in isolation
|
||||
export function advSchema (args) {
|
||||
return object({
|
||||
...advPostSchemaMembers(args)
|
||||
})
|
||||
}
|
||||
|
||||
export function bountySchema (client, me) {
|
||||
export function bountySchema (args) {
|
||||
return object({
|
||||
title: titleValidator,
|
||||
bounty: intValidator
|
||||
.min(1000, 'must be at least 1000')
|
||||
.max(1000000, 'must be at most 1m'),
|
||||
...advPostSchemaMembers(client, me),
|
||||
...advPostSchemaMembers(args),
|
||||
...subSelectSchemaMembers()
|
||||
})
|
||||
}
|
||||
|
||||
export function discussionSchema (client, me) {
|
||||
export function discussionSchema (args) {
|
||||
return object({
|
||||
title: titleValidator,
|
||||
...advPostSchemaMembers(client, me),
|
||||
...advPostSchemaMembers(args),
|
||||
...subSelectSchemaMembers()
|
||||
})
|
||||
}
|
||||
|
||||
export function linkSchema (client, me) {
|
||||
export function linkSchema (args) {
|
||||
return object({
|
||||
title: titleValidator,
|
||||
url: string().matches(URL_REGEXP, 'invalid url').required('required'),
|
||||
...advPostSchemaMembers(client, me),
|
||||
...advPostSchemaMembers(args),
|
||||
...subSelectSchemaMembers()
|
||||
})
|
||||
}
|
||||
|
||||
export function pollSchema (client, me, numExistingChoices = 0) {
|
||||
export function pollSchema ({ numExistingChoices = 0, ...args }) {
|
||||
return object({
|
||||
title: titleValidator,
|
||||
options: array().of(
|
||||
|
@ -151,12 +158,12 @@ export function pollSchema (client, me, numExistingChoices = 0) {
|
|||
message: `at least ${MIN_POLL_NUM_CHOICES} choices required`,
|
||||
test: arr => arr.length >= MIN_POLL_NUM_CHOICES - numExistingChoices
|
||||
}),
|
||||
...advPostSchemaMembers(client, me),
|
||||
...advPostSchemaMembers(args),
|
||||
...subSelectSchemaMembers()
|
||||
})
|
||||
}
|
||||
|
||||
export function userSchema (client) {
|
||||
export function userSchema (args) {
|
||||
return object({
|
||||
name: string()
|
||||
.required('required')
|
||||
|
@ -166,7 +173,7 @@ export function userSchema (client) {
|
|||
name: 'name',
|
||||
test: async name => {
|
||||
if (!name || !name.length) return false
|
||||
return !(await usernameExists(client, name))
|
||||
return !(await usernameExists(name, args))
|
||||
},
|
||||
message: 'taken'
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue