spam fees
This commit is contained in:
parent
7cf5396ef3
commit
ddb4a30c4b
|
@ -4,7 +4,8 @@ import serialize from './serial'
|
||||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||||
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
|
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
|
||||||
import domino from 'domino'
|
import domino from 'domino'
|
||||||
import { BOOST_MIN } from '../../lib/constants'
|
import { BOOST_MIN, ITEM_SPAM_INTERVAL } from '../../lib/constants'
|
||||||
|
import { mdHas } from '../../lib/md'
|
||||||
|
|
||||||
async function comments (models, id, sort) {
|
async function comments (models, id, sort) {
|
||||||
let orderBy
|
let orderBy
|
||||||
|
@ -68,6 +69,13 @@ function topClause (within) {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
|
itemRepetition: async (parent, { parentId }, { me, models }) => {
|
||||||
|
if (!me) return 0
|
||||||
|
// how many of the parents starting at parentId belong to me
|
||||||
|
const [{ item_spam: count }] = await models.$queryRaw(`SELECT item_spam($1, $2, '${ITEM_SPAM_INTERVAL}')`, Number(parentId), Number(me.id))
|
||||||
|
|
||||||
|
return count
|
||||||
|
},
|
||||||
items: async (parent, { sub, sort, cursor, name, within }, { me, models }) => {
|
items: async (parent, { sub, sort, cursor, name, within }, { me, models }) => {
|
||||||
const decodedCursor = decodeCursor(cursor)
|
const decodedCursor = decodeCursor(cursor)
|
||||||
let items; let user; let pins; let subFull
|
let items; let user; let pins; let subFull
|
||||||
|
@ -851,6 +859,10 @@ const updateItem = async (parent, { id, data }, { me, models }) => {
|
||||||
throw new UserInputError('item can no longer be editted')
|
throw new UserInputError('item can no longer be editted')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data?.text && !old.paidImgLink && mdHas(data.text, ['link', 'image'])) {
|
||||||
|
throw new UserInputError('adding links or images on edit is not allowed yet')
|
||||||
|
}
|
||||||
|
|
||||||
const item = await models.item.update({
|
const item = await models.item.update({
|
||||||
where: { id: Number(id) },
|
where: { id: Number(id) },
|
||||||
data
|
data
|
||||||
|
@ -878,21 +890,16 @@ const createItem = async (parent, { title, url, text, boost, forward, parentId }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasImgLink = mdHas(text, ['link', 'image'])
|
||||||
|
|
||||||
const [item] = await serialize(models,
|
const [item] = await serialize(models,
|
||||||
models.$queryRaw(`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6) AS "Item"`,
|
models.$queryRaw(
|
||||||
title, url, text, Number(boost || 0), Number(parentId), Number(me.id)))
|
`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6, $7, $8, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||||
|
title, url, text, Number(boost || 0), Number(parentId), Number(me.id),
|
||||||
|
Number(fwdUser?.id), hasImgLink))
|
||||||
|
|
||||||
await createMentions(item, models)
|
await createMentions(item, models)
|
||||||
|
|
||||||
if (fwdUser) {
|
|
||||||
await models.item.update({
|
|
||||||
where: { id: item.id },
|
|
||||||
data: {
|
|
||||||
fwdUserId: fwdUser.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
item.comments = []
|
item.comments = []
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export default gql`
|
||||||
allItems(cursor: String): Items
|
allItems(cursor: String): Items
|
||||||
search(q: String, sub: String, cursor: String): Items
|
search(q: String, sub: String, cursor: String): Items
|
||||||
auctionPosition(sub: String, id: ID, bid: Int!): Int!
|
auctionPosition(sub: String, id: ID, bid: Int!): Int!
|
||||||
|
itemRepetition(parentId: ID): Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type ItemActResult {
|
type ItemActResult {
|
||||||
|
|
|
@ -2,11 +2,12 @@ import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||||
import ActionTooltip from '../components/action-tooltip'
|
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
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 from './fee-button'
|
||||||
|
|
||||||
export function DiscussionForm ({
|
export function DiscussionForm ({
|
||||||
item, editThreshold, titleLabel = 'title',
|
item, editThreshold, titleLabel = 'title',
|
||||||
|
@ -15,6 +16,8 @@ export function DiscussionForm ({
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
|
const [hasImgLink, setHasImgLink] = useState()
|
||||||
|
// const me = useMe()
|
||||||
const [upsertDiscussion] = useMutation(
|
const [upsertDiscussion] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation upsertDiscussion($id: ID, $title: String!, $text: String, $boost: Int, $forward: String) {
|
mutation upsertDiscussion($id: ID, $title: String!, $text: String, $boost: Int, $forward: String) {
|
||||||
|
@ -31,6 +34,8 @@ export function DiscussionForm ({
|
||||||
...AdvPostSchema(client)
|
...AdvPostSchema(client)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// const cost = linkOrImg ? 10 : me?.freePosts ? 0 : 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form
|
<Form
|
||||||
initial={{
|
initial={{
|
||||||
|
@ -70,11 +75,17 @@ 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}
|
||||||
/>
|
/>
|
||||||
{!item && adv && <AdvPostForm />}
|
{!item && adv && <AdvPostForm />}
|
||||||
<ActionTooltip>
|
<div className='mt-3'>
|
||||||
<SubmitButton variant='secondary' className='mt-3'>{item ? 'save' : buttonText}</SubmitButton>
|
{item
|
||||||
</ActionTooltip>
|
? <SubmitButton variant='secondary'>save</SubmitButton>
|
||||||
|
: <FeeButton
|
||||||
|
baseFee={1} hasImgLink={hasImgLink} parentId={null} text={buttonText}
|
||||||
|
ChildButton={SubmitButton} variant='secondary'
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Table } from 'react-bootstrap'
|
||||||
|
import ActionTooltip from './action-tooltip'
|
||||||
|
import Info from './info'
|
||||||
|
import styles from './fee-button.module.css'
|
||||||
|
import { gql, useQuery } from '@apollo/client'
|
||||||
|
import { useFormikContext } from 'formik'
|
||||||
|
|
||||||
|
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
||||||
|
return (
|
||||||
|
<Table className={styles.receipt} borderless size='sm'>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{baseFee} sats</td>
|
||||||
|
<td align='right' className='font-weight-light'>{parentId ? 'reply' : 'post'} fee</td>
|
||||||
|
</tr>
|
||||||
|
{hasImgLink &&
|
||||||
|
<tr>
|
||||||
|
<td>x 10</td>
|
||||||
|
<td align='right' className='font-weight-light'>image/link fee</td>
|
||||||
|
</tr>}
|
||||||
|
{repetition > 0 &&
|
||||||
|
<tr>
|
||||||
|
<td>x 10<sup>{repetition}</sup></td>
|
||||||
|
<td className='font-weight-light' align='right'>{repetition} {parentId ? 'repeat or self replies' : 'posts'} in 10m</td>
|
||||||
|
</tr>}
|
||||||
|
{boost > 0 &&
|
||||||
|
<tr>
|
||||||
|
<td>+ {boost} sats</td>
|
||||||
|
<td className='font-weight-light' align='right'>boost</td>
|
||||||
|
</tr>}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td className='font-weight-bold'>{cost} sats</td>
|
||||||
|
<td align='right' className='font-weight-light'>total fee</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</Table>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton, variant, text, alwaysShow }) {
|
||||||
|
const query = parentId
|
||||||
|
? gql`{ itemRepetition(parentId: "${parentId}") }`
|
||||||
|
: gql`{ itemRepetition }`
|
||||||
|
const { data } = useQuery(query, { pollInterval: 1000 })
|
||||||
|
const repetition = data?.itemRepetition || 0
|
||||||
|
const formik = useFormikContext()
|
||||||
|
const boost = formik?.values?.boost || 0
|
||||||
|
const cost = baseFee * (hasImgLink ? 10 : 1) * Math.pow(10, repetition) + Number(boost)
|
||||||
|
|
||||||
|
const show = alwaysShow || !formik?.isSubmitting
|
||||||
|
return (
|
||||||
|
<div className='d-flex align-items-center'>
|
||||||
|
<ActionTooltip overlayText={`${cost} sats`}>
|
||||||
|
<ChildButton variant={variant}>{text}{cost > baseFee && show && <small> {cost} sats</small>}</ChildButton>
|
||||||
|
</ActionTooltip>
|
||||||
|
{cost > baseFee && show &&
|
||||||
|
<Info>
|
||||||
|
<Receipt baseFee={baseFee} hasImgLink={hasImgLink} repetition={repetition} cost={cost} parentId={parentId} boost={boost} />
|
||||||
|
</Info>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
.receipt {
|
||||||
|
background-color: var(--theme-inputBg);
|
||||||
|
max-width: 250px;
|
||||||
|
margin: auto;
|
||||||
|
table-layout: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receipt td {
|
||||||
|
padding: .25rem .1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receipt tfoot {
|
||||||
|
border-top: 2px solid var(--theme-borderColor);
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import Markdown from '../svgs/markdown-line.svg'
|
||||||
import styles from './form.module.css'
|
import styles from './form.module.css'
|
||||||
import Text from '../components/text'
|
import Text from '../components/text'
|
||||||
import AddIcon from '../svgs/add-fill.svg'
|
import AddIcon from '../svgs/add-fill.svg'
|
||||||
|
import { mdHas } from '../lib/md'
|
||||||
|
|
||||||
export function SubmitButton ({
|
export function SubmitButton ({
|
||||||
children, variant, value, onClick, ...props
|
children, variant, value, onClick, ...props
|
||||||
|
@ -72,7 +73,7 @@ export function InputSkeleton ({ label, hint }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MarkdownInput ({ label, topLevel, groupClassName, ...props }) {
|
export function MarkdownInput ({ label, topLevel, groupClassName, onChange, setHasImgLink, ...props }) {
|
||||||
const [tab, setTab] = useState('write')
|
const [tab, setTab] = useState('write')
|
||||||
const [, meta] = useField(props)
|
const [, meta] = useField(props)
|
||||||
|
|
||||||
|
@ -99,7 +100,12 @@ export function MarkdownInput ({ label, topLevel, groupClassName, ...props }) {
|
||||||
</Nav>
|
</Nav>
|
||||||
<div className={tab !== 'write' ? 'd-none' : ''}>
|
<div className={tab !== 'write' ? 'd-none' : ''}>
|
||||||
<InputInner
|
<InputInner
|
||||||
{...props}
|
{...props} onChange={(formik, e) => {
|
||||||
|
if (onChange) onChange(formik, e)
|
||||||
|
if (setHasImgLink) {
|
||||||
|
setHasImgLink(mdHas(e.target.value, ['link', 'image']))
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
|
<div className={tab !== 'preview' ? 'd-none' : 'form-group'}>
|
||||||
|
|
|
@ -2,13 +2,13 @@ import { Form, Input, SubmitButton } from '../components/form'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||||
import ActionTooltip from '../components/action-tooltip'
|
|
||||||
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 { ITEM_FIELDS } from '../fragments/items'
|
import { ITEM_FIELDS } from '../fragments/items'
|
||||||
import Item from './item'
|
import Item from './item'
|
||||||
import AccordianItem from './accordian-item'
|
import AccordianItem from './accordian-item'
|
||||||
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
import { MAX_TITLE_LENGTH } from '../lib/constants'
|
||||||
|
import FeeButton from './fee-button'
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const URL = /^((https?|ftp):\/\/)?(www.)?(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
|
const URL = /^((https?|ftp):\/\/)?(www.)?(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i
|
||||||
|
@ -99,9 +99,14 @@ export function LinkForm ({ item, editThreshold }) {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!item && <AdvPostForm />}
|
{!item && <AdvPostForm />}
|
||||||
<ActionTooltip>
|
<div className='mt-3'>
|
||||||
<SubmitButton variant='secondary' className='mt-3'>{item ? 'save' : 'post'}</SubmitButton>
|
{item
|
||||||
</ActionTooltip>
|
? <SubmitButton variant='secondary'>save</SubmitButton>
|
||||||
|
: <FeeButton
|
||||||
|
baseFee={1} parentId={null} text='post'
|
||||||
|
ChildButton={SubmitButton} variant='secondary'
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
{dupesData?.dupes?.length > 0 &&
|
{dupesData?.dupes?.length > 0 &&
|
||||||
<div className='mt-3'>
|
<div className='mt-3'>
|
||||||
<AccordianItem
|
<AccordianItem
|
||||||
|
|
|
@ -4,11 +4,10 @@ import { gql, useMutation } from '@apollo/client'
|
||||||
import styles from './reply.module.css'
|
import styles from './reply.module.css'
|
||||||
import { COMMENTS } from '../fragments/comments'
|
import { COMMENTS } from '../fragments/comments'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import ActionTooltip from './action-tooltip'
|
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
import TextareaAutosize from 'react-textarea-autosize'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import Info from './info'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import FeeButton from './fee-button'
|
||||||
|
|
||||||
export const CommentSchema = Yup.object({
|
export const CommentSchema = Yup.object({
|
||||||
text: Yup.string().required('required').trim()
|
text: Yup.string().required('required').trim()
|
||||||
|
@ -25,6 +24,7 @@ export function ReplyOnAnotherPage ({ parentId }) {
|
||||||
export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
|
export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
|
||||||
const [reply, setReply] = useState(replyOpen)
|
const [reply, setReply] = useState(replyOpen)
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
|
const [hasImgLink, setHasImgLink] = useState()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setReply(replyOpen || !!localStorage.getItem('reply-' + parentId + '-' + 'text'))
|
setReply(replyOpen || !!localStorage.getItem('reply-' + parentId + '-' + 'text'))
|
||||||
|
@ -65,7 +65,7 @@ export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const cost = me?.freeComments ? 0 : Math.pow(10, meComments)
|
// const cost = me?.freeComments ? 0 : Math.pow(10, meComments)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -91,6 +91,7 @@ export default function Reply ({ parentId, meComments, onSuccess, replyOpen }) {
|
||||||
}
|
}
|
||||||
resetForm({ text: '' })
|
resetForm({ text: '' })
|
||||||
setReply(replyOpen || false)
|
setReply(replyOpen || false)
|
||||||
|
setHasImgLink(false)
|
||||||
}}
|
}}
|
||||||
storageKeyPrefix={'reply-' + parentId}
|
storageKeyPrefix={'reply-' + parentId}
|
||||||
>
|
>
|
||||||
|
@ -100,18 +101,16 @@ export default function Reply ({ parentId, meComments, 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}
|
||||||
/>
|
/>
|
||||||
<div className='d-flex align-items-center mt-1'>
|
{reply &&
|
||||||
<ActionTooltip overlayText={`${cost} sats`}>
|
<div className='mt-1'>
|
||||||
<SubmitButton variant='secondary'>reply{cost > 1 && <small> {cost} sats</small>}</SubmitButton>
|
<FeeButton
|
||||||
</ActionTooltip>
|
baseFee={1} hasImgLink={hasImgLink} parentId={parentId} text='reply'
|
||||||
{cost > 1 && (
|
ChildButton={SubmitButton} variant='secondary' alwaysShow
|
||||||
<Info>
|
/>
|
||||||
<div className='font-weight-bold'>Multiple replies on the same level get pricier, but we still love your thoughts!</div>
|
</div>}
|
||||||
</Info>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -82,6 +82,11 @@ export default function Text ({ topLevel, noFragments, nofollow, children }) {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
a: ({ node, href, children, ...props }) => {
|
a: ({ node, href, children, ...props }) => {
|
||||||
|
if (children?.some(e => e?.props?.node?.tagName === 'img')) {
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
// map: fix any highlighted links
|
||||||
children = children?.map(e =>
|
children = children?.map(e =>
|
||||||
typeof e === 'string'
|
typeof e === 'string'
|
||||||
? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => {
|
? reactStringReplace(e, /:high\[([^\]]+)\]/g, (match, i) => {
|
||||||
|
|
|
@ -12,3 +12,4 @@ export const UPLOAD_TYPES_ALLOW = [
|
||||||
export const COMMENT_DEPTH_LIMIT = 10
|
export const COMMENT_DEPTH_LIMIT = 10
|
||||||
export const MAX_TITLE_LENGTH = 80
|
export const MAX_TITLE_LENGTH = 80
|
||||||
export const MAX_POLL_CHOICE_LENGTH = 30
|
export const MAX_POLL_CHOICE_LENGTH = 30
|
||||||
|
export const ITEM_SPAM_INTERVAL = '10m'
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { fromMarkdown } from 'mdast-util-from-markdown'
|
||||||
|
import { gfmFromMarkdown } from 'mdast-util-gfm'
|
||||||
|
import { visit } from 'unist-util-visit'
|
||||||
|
import { gfm } from 'micromark-extension-gfm'
|
||||||
|
|
||||||
|
export function mdHas (md, test) {
|
||||||
|
const tree = fromMarkdown(md, {
|
||||||
|
extensions: [gfm()],
|
||||||
|
mdastExtensions: [gfmFromMarkdown()]
|
||||||
|
})
|
||||||
|
|
||||||
|
let found = false
|
||||||
|
visit(tree, test, () => {
|
||||||
|
found = true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
|
@ -8,12 +8,12 @@ import { useState } from 'react'
|
||||||
import ItemFull from '../../components/item-full'
|
import ItemFull from '../../components/item-full'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { Form, MarkdownInput, SubmitButton } from '../../components/form'
|
import { Form, MarkdownInput, SubmitButton } from '../../components/form'
|
||||||
import ActionTooltip from '../../components/action-tooltip'
|
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
import TextareaAutosize from 'react-textarea-autosize'
|
||||||
import { useMe } from '../../components/me'
|
import { useMe } from '../../components/me'
|
||||||
import { USER_FULL } from '../../fragments/users'
|
import { USER_FULL } from '../../fragments/users'
|
||||||
import { ITEM_FIELDS } from '../../fragments/items'
|
import { ITEM_FIELDS } from '../../fragments/items'
|
||||||
import { getGetServerSideProps } from '../../api/ssrApollo'
|
import { getGetServerSideProps } from '../../api/ssrApollo'
|
||||||
|
import FeeButton from '../../components/fee-button'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps(USER_FULL, null,
|
export const getServerSideProps = getGetServerSideProps(USER_FULL, null,
|
||||||
data => !data.user)
|
data => !data.user)
|
||||||
|
@ -23,6 +23,8 @@ 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}
|
||||||
|
@ -68,10 +70,16 @@ export function BioForm ({ handleSuccess, bio }) {
|
||||||
name='bio'
|
name='bio'
|
||||||
as={TextareaAutosize}
|
as={TextareaAutosize}
|
||||||
minRows={6}
|
minRows={6}
|
||||||
|
setHasImgLink={setHasImgLink}
|
||||||
/>
|
/>
|
||||||
<ActionTooltip>
|
<div className='mt-3'>
|
||||||
<SubmitButton variant='secondary' className='mt-3'>{bio?.text ? 'save' : 'create'}</SubmitButton>
|
{bio?.text
|
||||||
</ActionTooltip>
|
? <SubmitButton variant='secondary'>save</SubmitButton>
|
||||||
|
: <FeeButton
|
||||||
|
baseFee={1} hasImgLink={hasImgLink} parentId={null} text='create'
|
||||||
|
ChildButton={SubmitButton} variant='secondary'
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Item" ADD COLUMN "paidImgLink" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ALTER COLUMN "freePosts" SET DEFAULT 0;
|
|
@ -0,0 +1,83 @@
|
||||||
|
CREATE OR REPLACE FUNCTION item_spam(parent_id INTEGER, user_id INTEGER, within INTERVAL)
|
||||||
|
RETURNS INTEGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
repeats INTEGER;
|
||||||
|
self_replies INTEGER;
|
||||||
|
BEGIN
|
||||||
|
SELECT count(*) INTO repeats
|
||||||
|
FROM "Item"
|
||||||
|
WHERE (parent_id IS NULL AND "parentId" IS NULL OR "parentId" = parent_id)
|
||||||
|
AND "userId" = user_id
|
||||||
|
AND created_at > now_utc() - within;
|
||||||
|
|
||||||
|
IF parent_id IS NULL THEN
|
||||||
|
RETURN repeats;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
WITH RECURSIVE base AS (
|
||||||
|
SELECT "Item".id, "Item"."parentId", "Item"."userId"
|
||||||
|
FROM "Item"
|
||||||
|
WHERE id = parent_id AND "userId" = user_id AND created_at > now_utc() - within
|
||||||
|
UNION ALL
|
||||||
|
SELECT "Item".id, "Item"."parentId", "Item"."userId"
|
||||||
|
FROM base p
|
||||||
|
JOIN "Item" ON "Item".id = p."parentId" AND "Item"."userId" = p."userId" AND "Item".created_at > now_utc() - within)
|
||||||
|
SELECT count(*) INTO self_replies FROM base;
|
||||||
|
|
||||||
|
RETURN repeats + self_replies;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION 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)
|
||||||
|
RETURNS "Item"
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
user_msats INTEGER;
|
||||||
|
cost INTEGER;
|
||||||
|
free_posts INTEGER;
|
||||||
|
free_comments INTEGER;
|
||||||
|
freebie BOOLEAN;
|
||||||
|
item "Item";
|
||||||
|
BEGIN
|
||||||
|
PERFORM ASSERT_SERIALIZED();
|
||||||
|
|
||||||
|
SELECT msats, "freePosts", "freeComments"
|
||||||
|
INTO user_msats, free_posts, free_comments
|
||||||
|
FROM users WHERE id = user_id;
|
||||||
|
|
||||||
|
freebie := (parent_id IS NULL AND free_posts > 0) OR (parent_id IS NOT NULL AND free_comments > 0);
|
||||||
|
cost := 1000 * POWER(10, item_spam(parent_id, user_id, spam_within)) * CASE WHEN has_img_link THEN 10 ELSE 1 END;
|
||||||
|
|
||||||
|
IF NOT freebie AND cost > user_msats THEN
|
||||||
|
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO "Item" (title, url, text, "userId", "parentId", "fwdUserId", "paidImgLink", created_at, updated_at)
|
||||||
|
VALUES (title, url, text, user_id, parent_id, fwd_user_id, has_img_link, 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;
|
||||||
|
$$;
|
|
@ -33,7 +33,7 @@ model User {
|
||||||
msats Int @default(0)
|
msats Int @default(0)
|
||||||
stackedMsats Int @default(0)
|
stackedMsats Int @default(0)
|
||||||
freeComments Int @default(0)
|
freeComments Int @default(0)
|
||||||
freePosts Int @default(2)
|
freePosts Int @default(0)
|
||||||
checkedNotesAt DateTime?
|
checkedNotesAt DateTime?
|
||||||
tipDefault Int @default(10)
|
tipDefault Int @default(10)
|
||||||
pubkey String? @unique
|
pubkey String? @unique
|
||||||
|
@ -166,6 +166,7 @@ model Item {
|
||||||
boost Int @default(0)
|
boost Int @default(0)
|
||||||
uploadId Int?
|
uploadId Int?
|
||||||
upload Upload?
|
upload Upload?
|
||||||
|
paidImgLink Boolean @default(false)
|
||||||
|
|
||||||
// if sub is null, this is the main sub
|
// if sub is null, this is the main sub
|
||||||
sub Sub? @relation(fields: [subName], references: [name])
|
sub Sub? @relation(fields: [subName], references: [name])
|
||||||
|
|
Loading…
Reference in New Issue