Add anon comments and posts (link, discussion, poll)
This commit is contained in:
parent
5415c6b0f6
commit
74893b09dd
@ -7,7 +7,8 @@ import domino from 'domino'
|
|||||||
import {
|
import {
|
||||||
BOOST_MIN, ITEM_SPAM_INTERVAL,
|
BOOST_MIN, ITEM_SPAM_INTERVAL,
|
||||||
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD,
|
MAX_TITLE_LENGTH, ITEM_FILTER_THRESHOLD,
|
||||||
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY
|
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||||
|
ANON_COMMENT_FEE, ANON_USER_ID, ANON_POST_FEE
|
||||||
} from '../../lib/constants'
|
} from '../../lib/constants'
|
||||||
import { msatsToSats } from '../../lib/format'
|
import { msatsToSats } from '../../lib/format'
|
||||||
import { parse } from 'tldts'
|
import { parse } from 'tldts'
|
||||||
@ -571,7 +572,7 @@ export default {
|
|||||||
if (id) {
|
if (id) {
|
||||||
return await updateItem(parent, { id, data }, { me, models })
|
return await updateItem(parent, { id, data }, { me, models })
|
||||||
} else {
|
} else {
|
||||||
return await createItem(parent, data, { me, models })
|
return await createItem(parent, data, { me, models, invoiceId: args.invoiceId })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
upsertDiscussion: async (parent, args, { me, models }) => {
|
upsertDiscussion: async (parent, args, { me, models }) => {
|
||||||
@ -582,7 +583,7 @@ export default {
|
|||||||
if (id) {
|
if (id) {
|
||||||
return await updateItem(parent, { id, data }, { me, models })
|
return await updateItem(parent, { id, data }, { me, models })
|
||||||
} else {
|
} else {
|
||||||
return await createItem(parent, data, { me, models })
|
return await createItem(parent, data, { me, models, invoiceId: args.invoiceId })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
upsertBounty: async (parent, args, { me, models }) => {
|
upsertBounty: async (parent, args, { me, models }) => {
|
||||||
@ -597,8 +598,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
|
upsertPoll: async (parent, { id, ...data }, { me, models }) => {
|
||||||
const { forward, sub, boost, title, text, options } = data
|
const { sub, forward, boost, title, text, options, invoiceId } = data
|
||||||
if (!me) {
|
let author = me
|
||||||
|
const trx = []
|
||||||
|
if (!me && invoiceId) {
|
||||||
|
const invoice = await checkInvoice(models, invoiceId, ANON_POST_FEE)
|
||||||
|
author = invoice.user
|
||||||
|
trx.push(models.invoice.delete({ where: { id: Number(invoiceId) } }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!author) {
|
||||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,7 +631,7 @@ export default {
|
|||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
const old = await models.item.findUnique({ where: { id: Number(id) } })
|
const old = await models.item.findUnique({ where: { id: Number(id) } })
|
||||||
if (Number(old.userId) !== Number(me?.id)) {
|
if (Number(old.userId) !== Number(author.id)) {
|
||||||
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
|
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
|
||||||
}
|
}
|
||||||
const [item] = await serialize(models,
|
const [item] = await serialize(models,
|
||||||
@ -633,9 +642,10 @@ export default {
|
|||||||
item.comments = []
|
item.comments = []
|
||||||
return item
|
return item
|
||||||
} else {
|
} else {
|
||||||
const [item] = await serialize(models,
|
const [query] = await serialize(models,
|
||||||
models.$queryRawUnsafe(`${SELECT} FROM create_poll($1, $2, $3, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7, $8::INTEGER, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
models.$queryRawUnsafe(`${SELECT} FROM create_poll($1, $2, $3, $4::INTEGER, $5::INTEGER, $6::INTEGER, $7, $8::INTEGER, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||||
sub || 'bitcoin', title, text, 1, Number(boost || 0), Number(me.id), options, Number(fwdUser?.id)))
|
sub || 'bitcoin', title, text, 1, Number(boost || 0), Number(author.id), options, Number(fwdUser?.id)), ...trx)
|
||||||
|
const item = trx.length > 0 ? query[0] : query
|
||||||
|
|
||||||
await createMentions(item, models)
|
await createMentions(item, models)
|
||||||
item.comments = []
|
item.comments = []
|
||||||
@ -679,13 +689,13 @@ export default {
|
|||||||
},
|
},
|
||||||
createComment: async (parent, data, { me, models }) => {
|
createComment: async (parent, data, { me, models }) => {
|
||||||
await ssValidate(commentSchema, data)
|
await ssValidate(commentSchema, data)
|
||||||
const item = await createItem(parent, data, { me, models })
|
const item = await createItem(parent, data, { me, models, invoiceId: data.invoiceId })
|
||||||
// fetch user to get up-to-date name
|
// fetch user to get up-to-date name
|
||||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
|
||||||
|
|
||||||
const parents = await models.$queryRawUnsafe(
|
const parents = await models.$queryRawUnsafe(
|
||||||
'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2',
|
'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2',
|
||||||
Number(item.parentId), Number(me.id))
|
Number(item.parentId), Number(user.id))
|
||||||
Promise.allSettled(
|
Promise.allSettled(
|
||||||
parents.map(({ userId }) => sendUserNotification(userId, {
|
parents.map(({ userId }) => sendUserNotification(userId, {
|
||||||
title: `@${user.name} replied to you`,
|
title: `@${user.name} replied to you`,
|
||||||
@ -1065,8 +1075,16 @@ export const updateItem = async (parent, { id, data: { sub, title, url, text, bo
|
|||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models }) => {
|
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceId }) => {
|
||||||
if (!me) {
|
let author = me
|
||||||
|
const trx = []
|
||||||
|
if (!me && invoiceId) {
|
||||||
|
const invoice = await checkInvoice(models, invoiceId, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
||||||
|
author = invoice.user
|
||||||
|
trx.push(models.invoice.delete({ where: { id: Number(invoiceId) } }))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!author) {
|
||||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1089,7 +1107,7 @@ const createItem = async (parent, { sub, title, url, text, boost, forward, bount
|
|||||||
url = await proxyImages(url)
|
url = await proxyImages(url)
|
||||||
text = await proxyImages(text)
|
text = await proxyImages(text)
|
||||||
|
|
||||||
const [item] = await serialize(
|
const [query] = await serialize(
|
||||||
models,
|
models,
|
||||||
models.$queryRawUnsafe(
|
models.$queryRawUnsafe(
|
||||||
`${SELECT} FROM create_item($1, $2, $3, $4, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::INTEGER, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
`${SELECT} FROM create_item($1, $2, $3, $4, $5::INTEGER, $6::INTEGER, $7::INTEGER, $8::INTEGER, $9::INTEGER, '${ITEM_SPAM_INTERVAL}') AS "Item"`,
|
||||||
@ -1100,8 +1118,10 @@ const createItem = async (parent, { sub, title, url, text, boost, forward, bount
|
|||||||
Number(boost || 0),
|
Number(boost || 0),
|
||||||
bounty ? Number(bounty) : null,
|
bounty ? Number(bounty) : null,
|
||||||
Number(parentId),
|
Number(parentId),
|
||||||
Number(me.id),
|
Number(author.id),
|
||||||
Number(fwdUser?.id)))
|
Number(fwdUser?.id)),
|
||||||
|
...trx)
|
||||||
|
const item = trx.length > 0 ? query[0] : query
|
||||||
|
|
||||||
await createMentions(item, models)
|
await createMentions(item, models)
|
||||||
|
|
||||||
|
@ -26,13 +26,13 @@ export default gql`
|
|||||||
bookmarkItem(id: ID): Item
|
bookmarkItem(id: ID): Item
|
||||||
subscribeItem(id: ID): Item
|
subscribeItem(id: ID): Item
|
||||||
deleteItem(id: ID): Item
|
deleteItem(id: ID): Item
|
||||||
upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String): Item!
|
upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String, invoiceId: ID): Item!
|
||||||
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: String): Item!
|
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: String, invoiceId: ID): Item!
|
||||||
upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int!, boost: Int, forward: String): Item!
|
upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int!, boost: Int, forward: String): Item!
|
||||||
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
|
||||||
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item!
|
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item!
|
||||||
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: String): Item!
|
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: String, invoiceId: ID): Item!
|
||||||
createComment(text: String!, parentId: ID!): Item!
|
createComment(text: String!, parentId: ID!, invoiceId: ID): Item!
|
||||||
updateComment(id: ID!, text: String!): Item!
|
updateComment(id: ID!, text: String!): Item!
|
||||||
dontLikeThis(id: ID!): Boolean!
|
dontLikeThis(id: ID!): Boolean!
|
||||||
act(id: ID!, sats: Int, invoiceId: ID): ItemActResult!
|
act(id: ID!, sats: Int, invoiceId: ID): ItemActResult!
|
||||||
|
@ -12,6 +12,9 @@ import Button from 'react-bootstrap/Button'
|
|||||||
import { discussionSchema } from '../lib/validate'
|
import { discussionSchema } from '../lib/validate'
|
||||||
import { SubSelectInitial } from './sub-select-form'
|
import { SubSelectInitial } from './sub-select-form'
|
||||||
import CancelButton from './cancel-button'
|
import CancelButton from './cancel-button'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useAnonymous } from '../lib/anonymous'
|
||||||
|
import { ANON_POST_FEE } from '../lib/constants'
|
||||||
|
|
||||||
export function DiscussionForm ({
|
export function DiscussionForm ({
|
||||||
item, sub, editThreshold, titleLabel = 'title',
|
item, sub, editThreshold, titleLabel = 'title',
|
||||||
@ -27,13 +30,32 @@ export function DiscussionForm ({
|
|||||||
// const me = useMe()
|
// const me = useMe()
|
||||||
const [upsertDiscussion] = useMutation(
|
const [upsertDiscussion] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: String) {
|
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: String, $invoiceId: ID) {
|
||||||
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward) {
|
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward, invoiceId: $invoiceId) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const submitUpsertDiscussion = useCallback(
|
||||||
|
async (_, boost, values, invoiceId) => {
|
||||||
|
const { error } = await upsertDiscussion({
|
||||||
|
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, ...values, invoiceId }
|
||||||
|
})
|
||||||
|
if (error) {
|
||||||
|
throw new Error({ message: error.toString() })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
await router.push(`/items/${item.id}`)
|
||||||
|
} else {
|
||||||
|
const prefix = sub?.name ? `/~${sub.name}` : ''
|
||||||
|
await router.push(prefix + '/recent')
|
||||||
|
}
|
||||||
|
}, [upsertDiscussion, router])
|
||||||
|
|
||||||
|
const anonUpsertDiscussion = useAnonymous(submitUpsertDiscussion)
|
||||||
|
|
||||||
const [getRelated, { data: relatedData }] = useLazyQuery(gql`
|
const [getRelated, { data: relatedData }] = useLazyQuery(gql`
|
||||||
${ITEM_FIELDS}
|
${ITEM_FIELDS}
|
||||||
query related($title: String!) {
|
query related($title: String!) {
|
||||||
@ -58,19 +80,7 @@ export function DiscussionForm ({
|
|||||||
}}
|
}}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
onSubmit={handleSubmit || (async ({ boost, ...values }) => {
|
onSubmit={handleSubmit || (async ({ boost, ...values }) => {
|
||||||
const { error } = await upsertDiscussion({
|
await anonUpsertDiscussion(ANON_POST_FEE, boost, values)
|
||||||
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, ...values }
|
|
||||||
})
|
|
||||||
if (error) {
|
|
||||||
throw new Error({ message: error.toString() })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item) {
|
|
||||||
await router.push(`/items/${item.id}`)
|
|
||||||
} else {
|
|
||||||
const prefix = sub?.name ? `/~${sub.name}` : ''
|
|
||||||
await router.push(prefix + '/recent')
|
|
||||||
}
|
|
||||||
})}
|
})}
|
||||||
storageKeyPrefix={item ? undefined : 'discussion'}
|
storageKeyPrefix={item ? undefined : 'discussion'}
|
||||||
>
|
>
|
||||||
|
@ -4,6 +4,8 @@ import Info from './info'
|
|||||||
import styles from './fee-button.module.css'
|
import styles from './fee-button.module.css'
|
||||||
import { gql, useQuery } from '@apollo/client'
|
import { gql, useQuery } from '@apollo/client'
|
||||||
import { useFormikContext } from 'formik'
|
import { useFormikContext } from 'formik'
|
||||||
|
import { useMe } from './me'
|
||||||
|
import { ANON_COMMENT_FEE, ANON_POST_FEE } from '../lib/constants'
|
||||||
|
|
||||||
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
||||||
return (
|
return (
|
||||||
@ -40,11 +42,13 @@ function Receipt ({ cost, repetition, hasImgLink, baseFee, parentId, boost }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton, variant, text, alwaysShow, disabled }) {
|
export default function FeeButton ({ parentId, hasImgLink, baseFee, ChildButton, variant, text, alwaysShow, disabled }) {
|
||||||
|
const me = useMe()
|
||||||
|
baseFee = me ? baseFee : (parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
|
||||||
const query = parentId
|
const query = parentId
|
||||||
? gql`{ itemRepetition(parentId: "${parentId}") }`
|
? gql`{ itemRepetition(parentId: "${parentId}") }`
|
||||||
: gql`{ itemRepetition }`
|
: gql`{ itemRepetition }`
|
||||||
const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
const { data } = useQuery(query, { pollInterval: 1000, nextFetchPolicy: 'cache-and-network' })
|
||||||
const repetition = data?.itemRepetition || 0
|
const repetition = me ? data?.itemRepetition || 0 : 0
|
||||||
const formik = useFormikContext()
|
const formik = useFormikContext()
|
||||||
const boost = Number(formik?.values?.boost) || 0
|
const boost = Number(formik?.values?.boost) || 0
|
||||||
const cost = baseFee * (hasImgLink ? 10 : 1) * Math.pow(10, repetition) + Number(boost)
|
const cost = baseFee * (hasImgLink ? 10 : 1) * Math.pow(10, repetition) + Number(boost)
|
||||||
|
@ -212,9 +212,6 @@ function NavItems ({ className, sub, prefix }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PostItem ({ className, prefix }) {
|
function PostItem ({ className, prefix }) {
|
||||||
const me = useMe()
|
|
||||||
if (!me) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={prefix + '/post'} className={`${className} btn btn-md btn-primary px-3 py-1 `}>
|
<Link href={prefix + '/post'} className={`${className} btn btn-md btn-primary px-3 py-1 `}>
|
||||||
post
|
post
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
import { Form, Input, SubmitButton } from '../components/form'
|
import { Form, Input, SubmitButton } from '../components/form'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||||
@ -14,6 +14,8 @@ import { linkSchema } from '../lib/validate'
|
|||||||
import Moon from '../svgs/moon-fill.svg'
|
import Moon from '../svgs/moon-fill.svg'
|
||||||
import { SubSelectInitial } from './sub-select-form'
|
import { SubSelectInitial } from './sub-select-form'
|
||||||
import CancelButton from './cancel-button'
|
import CancelButton from './cancel-button'
|
||||||
|
import { useAnonymous } from '../lib/anonymous'
|
||||||
|
import { ANON_POST_FEE } from '../lib/constants'
|
||||||
|
|
||||||
export function LinkForm ({ item, sub, editThreshold, children }) {
|
export function LinkForm ({ item, sub, editThreshold, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -66,13 +68,31 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||||||
|
|
||||||
const [upsertLink] = useMutation(
|
const [upsertLink] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: String) {
|
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: String, $invoiceId: ID) {
|
||||||
upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward) {
|
upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward, invoiceId: $invoiceId) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const submitUpsertLink = useCallback(
|
||||||
|
async (_, boost, title, values, invoiceId) => {
|
||||||
|
const { error } = await upsertLink({
|
||||||
|
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, title: title.trim(), invoiceId, ...values }
|
||||||
|
})
|
||||||
|
if (error) {
|
||||||
|
throw new Error({ message: error.toString() })
|
||||||
|
}
|
||||||
|
if (item) {
|
||||||
|
await router.push(`/items/${item.id}`)
|
||||||
|
} else {
|
||||||
|
const prefix = sub?.name ? `/~${sub.name}` : ''
|
||||||
|
await router.push(prefix + '/recent')
|
||||||
|
}
|
||||||
|
}, [upsertLink, router])
|
||||||
|
|
||||||
|
const anonUpsertLink = useAnonymous(submitUpsertLink)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.pageTitleAndUnshorted?.title) {
|
if (data?.pageTitleAndUnshorted?.title) {
|
||||||
setTitleOverride(data.pageTitleAndUnshorted.title)
|
setTitleOverride(data.pageTitleAndUnshorted.title)
|
||||||
@ -100,18 +120,7 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||||||
}}
|
}}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
onSubmit={async ({ boost, title, ...values }) => {
|
onSubmit={async ({ boost, title, ...values }) => {
|
||||||
const { error } = await upsertLink({
|
await anonUpsertLink(ANON_POST_FEE, boost, title, values)
|
||||||
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, title: title.trim(), ...values }
|
|
||||||
})
|
|
||||||
if (error) {
|
|
||||||
throw new Error({ message: error.toString() })
|
|
||||||
}
|
|
||||||
if (item) {
|
|
||||||
await router.push(`/items/${item.id}`)
|
|
||||||
} else {
|
|
||||||
const prefix = sub?.name ? `/~${sub.name}` : ''
|
|
||||||
await router.push(prefix + '/recent')
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
storageKeyPrefix={item ? undefined : 'link'}
|
storageKeyPrefix={item ? undefined : 'link'}
|
||||||
>
|
>
|
||||||
|
@ -3,13 +3,15 @@ import { useRouter } from 'next/router'
|
|||||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||||
import Countdown from './countdown'
|
import Countdown from './countdown'
|
||||||
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
import AdvPostForm, { AdvPostInitial } from './adv-post-form'
|
||||||
import { MAX_POLL_NUM_CHOICES } from '../lib/constants'
|
import { ANON_POST_FEE, MAX_POLL_NUM_CHOICES } from '../lib/constants'
|
||||||
import FeeButton, { EditFeeButton } from './fee-button'
|
import FeeButton, { EditFeeButton } from './fee-button'
|
||||||
import Delete from './delete'
|
import Delete from './delete'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
import { pollSchema } from '../lib/validate'
|
import { pollSchema } from '../lib/validate'
|
||||||
import { SubSelectInitial } from './sub-select-form'
|
import { SubSelectInitial } from './sub-select-form'
|
||||||
import CancelButton from './cancel-button'
|
import CancelButton from './cancel-button'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { useAnonymous } from '../lib/anonymous'
|
||||||
|
|
||||||
export function PollForm ({ item, sub, editThreshold, children }) {
|
export function PollForm ({ item, sub, editThreshold, children }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -19,14 +21,41 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||||||
const [upsertPoll] = useMutation(
|
const [upsertPoll] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String,
|
mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String,
|
||||||
$options: [String!]!, $boost: Int, $forward: String) {
|
$options: [String!]!, $boost: Int, $forward: String, $invoiceId: ID) {
|
||||||
upsertPoll(sub: $sub, id: $id, title: $title, text: $text,
|
upsertPoll(sub: $sub, id: $id, title: $title, text: $text,
|
||||||
options: $options, boost: $boost, forward: $forward) {
|
options: $options, boost: $boost, forward: $forward, invoiceId: $invoiceId) {
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const submitUpsertPoll = useCallback(
|
||||||
|
async (_, boost, title, options, values, invoiceId) => {
|
||||||
|
const optionsFiltered = options.slice(initialOptions?.length).filter(word => word.trim().length > 0)
|
||||||
|
const { error } = await upsertPoll({
|
||||||
|
variables: {
|
||||||
|
id: item?.id,
|
||||||
|
sub: item?.subName || sub?.name,
|
||||||
|
boost: boost ? Number(boost) : undefined,
|
||||||
|
title: title.trim(),
|
||||||
|
options: optionsFiltered,
|
||||||
|
...values,
|
||||||
|
invoiceId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (error) {
|
||||||
|
throw new Error({ message: error.toString() })
|
||||||
|
}
|
||||||
|
if (item) {
|
||||||
|
await router.push(`/items/${item.id}`)
|
||||||
|
} else {
|
||||||
|
const prefix = sub?.name ? `/~${sub.name}` : ''
|
||||||
|
await router.push(prefix + '/recent')
|
||||||
|
}
|
||||||
|
}, [upsertPoll, router])
|
||||||
|
|
||||||
|
const anonUpsertPoll = useAnonymous(submitUpsertPoll)
|
||||||
|
|
||||||
const initialOptions = item?.poll?.options.map(i => i.option)
|
const initialOptions = item?.poll?.options.map(i => i.option)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -40,26 +69,7 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||||||
}}
|
}}
|
||||||
schema={schema}
|
schema={schema}
|
||||||
onSubmit={async ({ boost, title, options, ...values }) => {
|
onSubmit={async ({ boost, title, options, ...values }) => {
|
||||||
const optionsFiltered = options.slice(initialOptions?.length).filter(word => word.trim().length > 0)
|
await anonUpsertPoll(ANON_POST_FEE, boost, title, options, values)
|
||||||
const { error } = await upsertPoll({
|
|
||||||
variables: {
|
|
||||||
id: item?.id,
|
|
||||||
sub: item?.subName || sub?.name,
|
|
||||||
boost: boost ? Number(boost) : undefined,
|
|
||||||
title: title.trim(),
|
|
||||||
options: optionsFiltered,
|
|
||||||
...values
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (error) {
|
|
||||||
throw new Error({ message: error.toString() })
|
|
||||||
}
|
|
||||||
if (item) {
|
|
||||||
await router.push(`/items/${item.id}`)
|
|
||||||
} else {
|
|
||||||
const prefix = sub?.name ? `/~${sub.name}` : ''
|
|
||||||
await router.push(prefix + '/recent')
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
storageKeyPrefix={item ? undefined : 'poll'}
|
storageKeyPrefix={item ? undefined : 'poll'}
|
||||||
>
|
>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import JobForm from './job-form'
|
import JobForm from './job-form'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Button from 'react-bootstrap/Button'
|
import Button from 'react-bootstrap/Button'
|
||||||
|
import Alert from 'react-bootstrap/Alert'
|
||||||
import AccordianItem from './accordian-item'
|
import AccordianItem from './accordian-item'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
@ -10,6 +11,7 @@ import { PollForm } from './poll-form'
|
|||||||
import { BountyForm } from './bounty-form'
|
import { BountyForm } from './bounty-form'
|
||||||
import SubSelect from './sub-select-form'
|
import SubSelect from './sub-select-form'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
function FreebieDialog () {
|
function FreebieDialog () {
|
||||||
return (
|
return (
|
||||||
@ -28,12 +30,24 @@ function FreebieDialog () {
|
|||||||
|
|
||||||
export function PostForm ({ type, sub, children }) {
|
export function PostForm ({ type, sub, children }) {
|
||||||
const me = useMe()
|
const me = useMe()
|
||||||
|
const [errorMessage, setErrorMessage] = useState()
|
||||||
|
|
||||||
const prefix = sub?.name ? `/~${sub.name}` : ''
|
const prefix = sub?.name ? `/~${sub.name}` : ''
|
||||||
|
|
||||||
|
const checkSession = useCallback((e) => {
|
||||||
|
if (!me) {
|
||||||
|
e.preventDefault()
|
||||||
|
setErrorMessage('you must be logged in')
|
||||||
|
}
|
||||||
|
}, [me, setErrorMessage])
|
||||||
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
return (
|
return (
|
||||||
<div className='align-items-center'>
|
<div className='position-relative align-items-center'>
|
||||||
|
{errorMessage &&
|
||||||
|
<Alert className='position-absolute' style={{ top: '-6rem' }} variant='danger' onClose={() => setErrorMessage(undefined)} dismissible>
|
||||||
|
{errorMessage}
|
||||||
|
</Alert>}
|
||||||
{me?.sats < 1 && <FreebieDialog />}
|
{me?.sats < 1 && <FreebieDialog />}
|
||||||
<SubSelect noForm sub={sub?.name} />
|
<SubSelect noForm sub={sub?.name} />
|
||||||
<Link href={prefix + '/post?type=link'}>
|
<Link href={prefix + '/post?type=link'}>
|
||||||
@ -54,11 +68,11 @@ export function PostForm ({ type, sub, children }) {
|
|||||||
</Link>
|
</Link>
|
||||||
<span className='mx-3 fw-bold text-muted'>or</span>
|
<span className='mx-3 fw-bold text-muted'>or</span>
|
||||||
<Link href={prefix + '/post?type=bounty'}>
|
<Link href={prefix + '/post?type=bounty'}>
|
||||||
<Button variant='info'>bounty</Button>
|
<Button onClick={checkSession} variant='info'>bounty</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<div className='mt-3 d-flex justify-content-center'>
|
<div className='mt-3 d-flex justify-content-center'>
|
||||||
<Link href='/~jobs/post'>
|
<Link href='/~jobs/post'>
|
||||||
<Button variant='info'>job</Button>
|
<Button onClick={checkSession} variant='info'>job</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,12 +3,14 @@ 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 { useEffect, useState, useRef } from 'react'
|
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import FeeButton from './fee-button'
|
import FeeButton from './fee-button'
|
||||||
import { commentsViewedAfterComment } from '../lib/new-comments'
|
import { commentsViewedAfterComment } from '../lib/new-comments'
|
||||||
import { commentSchema } from '../lib/validate'
|
import { commentSchema } from '../lib/validate'
|
||||||
import Info from './info'
|
import Info from './info'
|
||||||
|
import { useAnonymous } from '../lib/anonymous'
|
||||||
|
import { ANON_COMMENT_FEE } from '../lib/constants'
|
||||||
|
|
||||||
export function ReplyOnAnotherPage ({ parentId }) {
|
export function ReplyOnAnotherPage ({ parentId }) {
|
||||||
return (
|
return (
|
||||||
@ -45,8 +47,8 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
|||||||
const [createComment] = useMutation(
|
const [createComment] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
${COMMENTS}
|
${COMMENTS}
|
||||||
mutation createComment($text: String!, $parentId: ID!) {
|
mutation createComment($text: String!, $parentId: ID!, $invoiceId: ID) {
|
||||||
createComment(text: $text, parentId: $parentId) {
|
createComment(text: $text, parentId: $parentId, invoiceId: $invoiceId) {
|
||||||
...CommentFields
|
...CommentFields
|
||||||
comments {
|
comments {
|
||||||
...CommentsRecursive
|
...CommentsRecursive
|
||||||
@ -90,6 +92,18 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const submitComment = useCallback(
|
||||||
|
async (_, values, parentId, resetForm, invoiceId) => {
|
||||||
|
const { error } = await createComment({ variables: { ...values, parentId, invoiceId } })
|
||||||
|
if (error) {
|
||||||
|
throw new Error({ message: error.toString() })
|
||||||
|
}
|
||||||
|
resetForm({ text: '' })
|
||||||
|
setReply(replyOpen || false)
|
||||||
|
}, [createComment, setReply])
|
||||||
|
|
||||||
|
const anonCreateComment = useAnonymous(submitComment)
|
||||||
|
|
||||||
const replyInput = useRef(null)
|
const replyInput = useRef(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (replyInput.current && reply && !replyOpen) replyInput.current.focus()
|
if (replyInput.current && reply && !replyOpen) replyInput.current.focus()
|
||||||
@ -117,12 +131,7 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
|||||||
}}
|
}}
|
||||||
schema={commentSchema}
|
schema={commentSchema}
|
||||||
onSubmit={async (values, { resetForm }) => {
|
onSubmit={async (values, { resetForm }) => {
|
||||||
const { error } = await createComment({ variables: { ...values, parentId } })
|
await anonCreateComment(ANON_COMMENT_FEE, values, parentId, resetForm)
|
||||||
if (error) {
|
|
||||||
throw new Error({ message: error.toString() })
|
|
||||||
}
|
|
||||||
resetForm({ text: '' })
|
|
||||||
setReply(replyOpen || false)
|
|
||||||
}}
|
}}
|
||||||
storageKeyPrefix={'reply-' + parentId}
|
storageKeyPrefix={'reply-' + parentId}
|
||||||
>
|
>
|
||||||
|
@ -58,7 +58,7 @@ export const useAnonymous = (fn) => {
|
|||||||
if (me) return fn(amount, ...args)
|
if (me) return fn(amount, ...args)
|
||||||
setFnArgs(args)
|
setFnArgs(args)
|
||||||
return createInvoice({ variables: { amount } })
|
return createInvoice({ variables: { amount } })
|
||||||
})
|
}, [fn, setFnArgs, createInvoice])
|
||||||
|
|
||||||
return anonFn
|
return anonFn
|
||||||
}
|
}
|
||||||
|
@ -46,3 +46,5 @@ export const ITEM_TYPES = context => {
|
|||||||
export const OLD_ITEM_DAYS = 3
|
export const OLD_ITEM_DAYS = 3
|
||||||
|
|
||||||
export const ANON_USER_ID = 27
|
export const ANON_USER_ID = 27
|
||||||
|
export const ANON_POST_FEE = 1000
|
||||||
|
export const ANON_COMMENT_FEE = 100
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
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
|
||||||
|
IF user_id = 27 THEN
|
||||||
|
-- disable fee escalation for anon user
|
||||||
|
RETURN 0;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
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;
|
||||||
|
$$;
|
Loading…
x
Reference in New Issue
Block a user