diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 95ca9d4d..27772fa4 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -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"` diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index a9abc972..fb752e82 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -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, diff --git a/api/typeDefs/item.js b/api/typeDefs/item.js index 6e00fe79..642069a1 100644 --- a/api/typeDefs/item.js +++ b/api/typeDefs/item.js @@ -52,6 +52,7 @@ export default gql` root: Item user: User! userId: Int! + fwdUser: User depth: Int! mine: Boolean! boost: Int! diff --git a/components/adv-post-form.js b/components/adv-post-form.js index 2b314fa9..5328e2df 100644 --- a/components/adv-post-form.js +++ b/components/adv-post-form.js @@ -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 = { - 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() +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() + .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 () { 100% of sats earned will be sent to this user} + hint={100% of sats will be sent to this user} prepend=@ + showValid /> } diff --git a/components/discussion-form.js b/components/discussion-form.js index 7ea2d5c2..3f72a4b8 100644 --- a/components/discussion-form.js +++ b/components/discussion-form.js @@ -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 (
{append && ( diff --git a/components/item-full.js b/components/item-full.js index 5916dce9..6a83c46d 100644 --- a/components/item-full.js +++ b/components/item-full.js @@ -83,7 +83,7 @@ function TopLevelItem ({ item, noReply, ...props }) { const ItemComponent = item.maxBid ? ItemJob : Item return ( - + {item.text && } {item.url && } {!noReply && } diff --git a/components/item.js b/components/item.js index d49efc50..17171e69 100644 --- a/components/item.js +++ b/components/item.js @@ -97,7 +97,18 @@ export function ItemJob ({ item, rank, children }) { ) } -export default function Item ({ item, rank, children }) { +function FwdUser ({ user }) { + return ( +
+ 100% of tips are forwarded to{' '} + + @{user.name} + +
+ ) +} + +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 }) { } + {showFwdUser && item.fwdUser && } {children && ( diff --git a/components/link-form.js b/components/link-form.js index 7e129c36..53ba17a1 100644 --- a/components/link-form.js +++ b/components/link-form.js @@ -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 ( {n.__typename === 'Votification' && - 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}`} } {n.__typename === 'Mention' && diff --git a/components/upvote.js b/components/upvote.js index 08217c97..70ecfb2a 100644 --- a/components/upvote.js +++ b/components/upvote.js @@ -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 } > - +
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; +$$; \ No newline at end of file