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 { | ||||
|  | ||||
							
								
								
									
										49
									
								
								prisma/migrations/20220419162551_fwd_item_act/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								prisma/migrations/20220419162551_fwd_item_act/migration.sql
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user