forward tips from posts
This commit is contained in:
parent
822fa9113a
commit
d978ff5ea5
|
@ -611,6 +611,12 @@ export default {
|
|||
},
|
||||
user: async (item, args, { models }) =>
|
||||
await models.user.findUnique({ where: { id: item.userId } }),
|
||||
fwdUser: async (item, args, { models }) => {
|
||||
if (!item.fwdUserId) {
|
||||
return null
|
||||
}
|
||||
return await models.user.findUnique({ where: { id: item.fwdUserId } })
|
||||
},
|
||||
ncomments: async (item, args, { models }) => {
|
||||
const [{ count }] = await models.$queryRaw`
|
||||
SELECT count(*)
|
||||
|
@ -793,12 +799,29 @@ const createItem = async (parent, { title, url, text, boost, forward, parentId }
|
|||
throw new UserInputError(`boost must be at least ${BOOST_MIN}`, { argumentName: 'boost' })
|
||||
}
|
||||
|
||||
let fwdUser
|
||||
if (forward) {
|
||||
fwdUser = await models.user.findUnique({ where: { name: forward } })
|
||||
if (!fwdUser) {
|
||||
throw new UserInputError('forward user does not exist', { argumentName: 'forward' })
|
||||
}
|
||||
}
|
||||
|
||||
const [item] = await serialize(models,
|
||||
models.$queryRaw(`${SELECT} FROM create_item($1, $2, $3, $4, $5, $6) AS "Item"`,
|
||||
title, url, text, Number(boost || 0), Number(parentId), Number(me.id)))
|
||||
|
||||
await createMentions(item, models)
|
||||
|
||||
if (fwdUser) {
|
||||
await models.item.update({
|
||||
where: { id: item.id },
|
||||
data: {
|
||||
fwdUserId: fwdUser.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
item.comments = []
|
||||
return item
|
||||
}
|
||||
|
@ -831,7 +854,7 @@ function nestComments (flat, parentId) {
|
|||
// we have to do our own query because ltree is unsupported
|
||||
export const SELECT =
|
||||
`SELECT "Item".id, "Item".created_at as "createdAt", "Item".updated_at as "updatedAt", "Item".title,
|
||||
"Item".text, "Item".url, "Item"."userId", "Item"."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"."subName", "Item".status, ltree2text("Item"."path") AS "path"`
|
||||
|
||||
|
|
|
@ -97,7 +97,9 @@ export default {
|
|||
FROM "ItemAct"
|
||||
JOIN "Item" on "ItemAct"."itemId" = "Item".id
|
||||
WHERE "ItemAct"."userId" <> $1 AND "ItemAct".act <> 'BOOST'
|
||||
AND "Item"."userId" = $1 AND "ItemAct".created_at <= $2
|
||||
AND (("Item"."userId" = $1 AND "Item"."fwdUserId" IS NULL)
|
||||
OR ("Item"."fwdUserId" = $1 AND "ItemAct"."userId" <> "Item"."userId"))
|
||||
AND "ItemAct".created_at <= $2
|
||||
GROUP BY "Item".id)`)
|
||||
queries.push(
|
||||
`(SELECT ('earn' || "Earn".id) as id, "Earn".id as "factId", NULL as bolt11,
|
||||
|
|
|
@ -52,6 +52,7 @@ export default gql`
|
|||
root: Item
|
||||
user: User!
|
||||
userId: Int!
|
||||
fwdUser: User
|
||||
depth: Int!
|
||||
mine: Boolean!
|
||||
boost: Int!
|
||||
|
|
|
@ -3,11 +3,23 @@ import * as Yup from 'yup'
|
|||
import { Input } from './form'
|
||||
import { InputGroup } from 'react-bootstrap'
|
||||
import { BOOST_MIN } from '../lib/constants'
|
||||
import { NAME_QUERY } from '../fragments/users'
|
||||
|
||||
export const AdvPostSchema = {
|
||||
export function AdvPostSchema (client) {
|
||||
return {
|
||||
boost: Yup.number().typeError('must be a number')
|
||||
.min(BOOST_MIN, `must be at least ${BOOST_MIN}`).integer('must be whole'),
|
||||
forward: Yup.string().trim()
|
||||
forward: Yup.string()
|
||||
.test({
|
||||
name: 'name',
|
||||
test: async name => {
|
||||
if (!name || !name.length) return true
|
||||
const { data } = await client.query({ query: NAME_QUERY, variables: { name }, fetchPolicy: 'network-only' })
|
||||
return !data.nameAvailable
|
||||
},
|
||||
message: 'user does not exist'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const AdvPostInitial = {
|
||||
|
@ -30,8 +42,9 @@ export default function AdvPostForm () {
|
|||
<Input
|
||||
label='forward sats to'
|
||||
name='forward'
|
||||
hint={<span className='text-muted'>100% of sats earned will be sent to this user</span>}
|
||||
hint={<span className='text-muted'>100% of sats will be sent to this user</span>}
|
||||
prepend=<InputGroup.Text>@</InputGroup.Text>
|
||||
showValid
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Form, Input, MarkdownInput, SubmitButton } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
import ActionTooltip from '../components/action-tooltip'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import Countdown from './countdown'
|
||||
|
@ -18,6 +18,7 @@ export function DiscussionForm ({
|
|||
adv, handleSubmit
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
const [upsertDiscussion] = useMutation(
|
||||
gql`
|
||||
mutation upsertDiscussion($id: ID, $title: String!, $text: String, $boost: Int, $forward: String) {
|
||||
|
@ -27,6 +28,11 @@ export function DiscussionForm ({
|
|||
}`
|
||||
)
|
||||
|
||||
const DiscussionSchema = Yup.object({
|
||||
title: Yup.string().required('required').trim(),
|
||||
...AdvPostSchema(client)
|
||||
})
|
||||
|
||||
return (
|
||||
<Form
|
||||
initial={{
|
||||
|
|
|
@ -173,7 +173,7 @@ function InputInner ({
|
|||
}
|
||||
}}
|
||||
isInvalid={meta.touched && meta.error}
|
||||
isValid={showValid && meta.touched && !meta.error}
|
||||
isValid={showValid && meta.initialValue !== meta.value && meta.touched && !meta.error}
|
||||
/>
|
||||
{append && (
|
||||
<InputGroup.Append>
|
||||
|
|
|
@ -83,7 +83,7 @@ function TopLevelItem ({ item, noReply, ...props }) {
|
|||
const ItemComponent = item.maxBid ? ItemJob : Item
|
||||
|
||||
return (
|
||||
<ItemComponent item={item} {...props}>
|
||||
<ItemComponent item={item} showFwdUser {...props}>
|
||||
{item.text && <ItemText item={item} />}
|
||||
{item.url && <ItemEmbed item={item} />}
|
||||
{!noReply && <Reply parentId={item.id} meComments={item.meComments} replyOpen />}
|
||||
|
|
|
@ -97,7 +97,18 @@ export function ItemJob ({ item, rank, children }) {
|
|||
)
|
||||
}
|
||||
|
||||
export default function Item ({ item, rank, children }) {
|
||||
function FwdUser ({ user }) {
|
||||
return (
|
||||
<div className={styles.other}>
|
||||
100% of tips are forwarded to{' '}
|
||||
<Link href={`/${user.name}`} passHref>
|
||||
<a>@{user.name}</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Item ({ item, rank, showFwdUser, children }) {
|
||||
const mine = item.mine
|
||||
const editThreshold = new Date(item.createdAt).getTime() + 10 * 60000
|
||||
const [canEdit, setCanEdit] =
|
||||
|
@ -187,6 +198,7 @@ export default function Item ({ item, rank, children }) {
|
|||
</Link>
|
||||
</>}
|
||||
</div>
|
||||
{showFwdUser && item.fwdUser && <FwdUser user={item.fwdUser} />}
|
||||
</div>
|
||||
</div>
|
||||
{children && (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Form, Input, SubmitButton } from '../components/form'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as Yup from 'yup'
|
||||
import { gql, useLazyQuery, useMutation } from '@apollo/client'
|
||||
import { gql, useApolloClient, useLazyQuery, useMutation } from '@apollo/client'
|
||||
import ActionTooltip from '../components/action-tooltip'
|
||||
import Countdown from './countdown'
|
||||
import AdvPostForm, { AdvPostInitial, AdvPostSchema } from './adv-post-form'
|
||||
|
@ -11,14 +11,10 @@ import AccordianItem from './accordian-item'
|
|||
|
||||
// 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
|
||||
export const LinkSchema = Yup.object({
|
||||
title: Yup.string().required('required').trim(),
|
||||
url: Yup.string().matches(URL, 'invalid url').required('required'),
|
||||
...AdvPostSchema
|
||||
})
|
||||
|
||||
export function LinkForm ({ item, editThreshold }) {
|
||||
const router = useRouter()
|
||||
const client = useApolloClient()
|
||||
|
||||
const [getPageTitle, { data }] = useLazyQuery(gql`
|
||||
query PageTitle($url: String!) {
|
||||
|
@ -45,6 +41,12 @@ export function LinkForm ({ item, editThreshold }) {
|
|||
}`
|
||||
)
|
||||
|
||||
const LinkSchema = Yup.object({
|
||||
title: Yup.string().required('required').trim(),
|
||||
url: Yup.string().matches(URL, 'invalid url').required('required'),
|
||||
...AdvPostSchema(client)
|
||||
})
|
||||
|
||||
return (
|
||||
<Form
|
||||
initial={{
|
||||
|
|
|
@ -79,7 +79,7 @@ function Notification ({ n }) {
|
|||
<>
|
||||
{n.__typename === 'Votification' &&
|
||||
<small className='font-weight-bold text-success ml-2'>
|
||||
your {n.item.title ? 'post' : 'reply'} stacked {n.earnedSats} sats
|
||||
your {n.item.title ? 'post' : 'reply'} {n.item.fwdUser ? 'forwarded' : 'stacked'} {n.earnedSats} sats{n.item.fwdUser && ` to @${n.item.fwdUser.name}`}
|
||||
</small>}
|
||||
{n.__typename === 'Mention' &&
|
||||
<small className='font-weight-bold text-info ml-2'>
|
||||
|
|
|
@ -76,6 +76,8 @@ export default function UpVote ({ item, className }) {
|
|||
}`
|
||||
)
|
||||
|
||||
const fwd2me = me && me?.id === item?.fwdUser?.id
|
||||
|
||||
const setVoteShow = (yes) => {
|
||||
if (!me) return
|
||||
|
||||
|
@ -155,7 +157,7 @@ export default function UpVote ({ item, className }) {
|
|||
if (!item) return
|
||||
|
||||
// we can't tip ourselves
|
||||
if (item?.mine) {
|
||||
if (item?.mine || fwd2me) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -169,7 +171,7 @@ export default function UpVote ({ item, className }) {
|
|||
if (!item) return
|
||||
|
||||
// we can't tip ourselves
|
||||
if (item?.mine) {
|
||||
if (item?.mine || fwd2me) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -201,9 +203,9 @@ export default function UpVote ({ item, className }) {
|
|||
: signIn
|
||||
}
|
||||
>
|
||||
<ActionTooltip notForm disable={item?.mine} overlayText={overlayText()}>
|
||||
<ActionTooltip notForm disable={item?.mine || fwd2me} overlayText={overlayText()}>
|
||||
<div
|
||||
className={`${item?.mine ? styles.noSelfTips : ''}
|
||||
className={`${item?.mine || fwd2me ? styles.noSelfTips : ''}
|
||||
${styles.upvoteWrapper}`}
|
||||
>
|
||||
<UpBolt
|
||||
|
@ -212,7 +214,7 @@ export default function UpVote ({ item, className }) {
|
|||
className={
|
||||
`${styles.upvote}
|
||||
${className || ''}
|
||||
${item?.mine ? styles.noSelfTips : ''}
|
||||
${item?.mine || fwd2me ? styles.noSelfTips : ''}
|
||||
${item?.meSats ? styles.voted : ''}`
|
||||
}
|
||||
style={item?.meSats
|
||||
|
|
|
@ -9,20 +9,7 @@ import * as Yup from 'yup'
|
|||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||
import styles from './user-header.module.css'
|
||||
import { useMe } from './me'
|
||||
|
||||
const NAME_QUERY =
|
||||
gql`
|
||||
query nameAvailable($name: String!) {
|
||||
nameAvailable(name: $name)
|
||||
}
|
||||
`
|
||||
|
||||
const NAME_MUTATION =
|
||||
gql`
|
||||
mutation setName($name: String!) {
|
||||
setName(name: $name)
|
||||
}
|
||||
`
|
||||
import { NAME_MUTATION, NAME_QUERY } from '../fragments/users'
|
||||
|
||||
export default function UserHeader ({ user }) {
|
||||
const [editting, setEditting] = useState(false)
|
||||
|
|
|
@ -12,6 +12,10 @@ export const ITEM_FIELDS = gql`
|
|||
name
|
||||
id
|
||||
}
|
||||
fwdUser {
|
||||
name
|
||||
id
|
||||
}
|
||||
sats
|
||||
upvotes
|
||||
boost
|
||||
|
|
|
@ -22,6 +22,20 @@ export const ME = gql`
|
|||
}
|
||||
}`
|
||||
|
||||
export const NAME_QUERY =
|
||||
gql`
|
||||
query nameAvailable($name: String!) {
|
||||
nameAvailable(name: $name)
|
||||
}
|
||||
`
|
||||
|
||||
export const NAME_MUTATION =
|
||||
gql`
|
||||
mutation setName($name: String!) {
|
||||
setName(name: $name)
|
||||
}
|
||||
`
|
||||
|
||||
export const USER_FIELDS = gql`
|
||||
${ITEM_FIELDS}
|
||||
fragment UserFields on User {
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
CREATE OR REPLACE FUNCTION item_act(item_id INTEGER, user_id INTEGER, act "ItemActType", act_sats INTEGER)
|
||||
RETURNS INTEGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
user_sats INTEGER;
|
||||
BEGIN
|
||||
PERFORM ASSERT_SERIALIZED();
|
||||
|
||||
SELECT (msats / 1000) INTO user_sats FROM users WHERE id = user_id;
|
||||
IF act_sats > user_sats THEN
|
||||
RAISE EXCEPTION 'SN_INSUFFICIENT_FUNDS';
|
||||
END IF;
|
||||
|
||||
-- deduct sats from actor
|
||||
UPDATE users SET msats = msats - (act_sats * 1000) WHERE id = user_id;
|
||||
|
||||
IF act = 'BOOST' THEN
|
||||
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (act_sats, item_id, user_id, 'BOOST', now_utc(), now_utc());
|
||||
ELSE
|
||||
-- add sats to actee's balance and stacked count
|
||||
UPDATE users
|
||||
SET msats = msats + (act_sats * 1000), "stackedMsats" = "stackedMsats" + (act_sats * 1000)
|
||||
WHERE id = (SELECT COALESCE("fwdUserId", "userId") FROM "Item" WHERE id = item_id);
|
||||
|
||||
-- if they have already voted, this is a tip
|
||||
IF EXISTS (SELECT 1 FROM "ItemAct" WHERE "itemId" = item_id AND "userId" = user_id AND "ItemAct".act = 'VOTE') THEN
|
||||
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (act_sats, item_id, user_id, 'TIP', now_utc(), now_utc());
|
||||
ELSE
|
||||
-- else this is a vote with a possible extra tip
|
||||
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (1, item_id, user_id, 'VOTE', now_utc(), now_utc());
|
||||
act_sats := act_sats - 1;
|
||||
|
||||
-- if we have sats left after vote, leave them as a tip
|
||||
IF act_sats > 0 THEN
|
||||
INSERT INTO "ItemAct" (sats, "itemId", "userId", act, created_at, updated_at)
|
||||
VALUES (act_sats, item_id, user_id, 'TIP', now_utc(), now_utc());
|
||||
END IF;
|
||||
|
||||
RETURN 1;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN 0;
|
||||
END;
|
||||
$$;
|
Loading…
Reference in New Issue