unify zap/downzap/bounty mutation
This commit is contained in:
parent
67a9fe23cf
commit
374a7985da
|
@ -7,14 +7,14 @@ import { ruleSet as publicationDateRuleSet } from '../../lib/timedate-scraper'
|
|||
import domino from 'domino'
|
||||
import {
|
||||
ITEM_SPAM_INTERVAL, ITEM_FILTER_THRESHOLD,
|
||||
DONT_LIKE_THIS_COST, COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||
COMMENT_DEPTH_LIMIT, COMMENT_TYPE_QUERY,
|
||||
ANON_USER_ID, ANON_ITEM_SPAM_INTERVAL, POLL_COST,
|
||||
ITEM_ALLOW_EDITS, GLOBAL_SEED, ANON_FEE_MULTIPLIER
|
||||
} from '../../lib/constants'
|
||||
import { msatsToSats } from '../../lib/format'
|
||||
import { parse } from 'tldts'
|
||||
import uu from 'url-unshort'
|
||||
import { advSchema, amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
|
||||
import { actSchema, advSchema, bountySchema, commentSchema, discussionSchema, jobSchema, linkSchema, pollSchema, ssValidate } from '../../lib/validate'
|
||||
import { sendUserNotification } from '../webPush'
|
||||
import { defaultCommentSort, isJob, deleteItemByAuthor, getDeleteCommand, hasDeleteCommand } from '../../lib/item'
|
||||
import { notifyItemParents, notifyUserSubscribers, notifyZapped } from '../../lib/push-notifications'
|
||||
|
@ -478,6 +478,10 @@ export default {
|
|||
${whereClause(
|
||||
subClause(sub, 3, 'Item', true),
|
||||
muteClause(me),
|
||||
'"Item"."pinId" IS NULL',
|
||||
'"Item"."deletedAt" IS NULL',
|
||||
'"Item"."parentId" IS NULL',
|
||||
'"Item".bio = false',
|
||||
await filterClause(me, models, type))}
|
||||
ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".freebie IS FALSE) DESC, "Item".id DESC
|
||||
OFFSET $1
|
||||
|
@ -748,8 +752,8 @@ export default {
|
|||
|
||||
return id
|
||||
},
|
||||
act: async (parent, { id, sats, hash, hmac }, { me, models, lnd, headers }) => {
|
||||
await ssValidate(amountSchema, { amount: sats })
|
||||
act: async (parent, { id, sats, act = 'TIP', hash, hmac }, { me, models, lnd, headers }) => {
|
||||
await ssValidate(actSchema, { sats, act })
|
||||
await assertGofacYourself({ models, headers })
|
||||
|
||||
const [item] = await models.$queryRawUnsafe(`
|
||||
|
@ -764,9 +768,11 @@ export default {
|
|||
}
|
||||
|
||||
// Disallow tips if me is one of the forward user recipients
|
||||
const existingForwards = await models.itemForward.findMany({ where: { itemId: Number(id) } })
|
||||
if (existingForwards.some(fwd => Number(fwd.userId) === Number(me.id))) {
|
||||
throw new GraphQLError('cannot zap a post for which you are forwarded zaps', { extensions: { code: 'BAD_INPUT' } })
|
||||
if (act === 'TIP') {
|
||||
const existingForwards = await models.itemForward.findMany({ where: { itemId: Number(id) } })
|
||||
if (existingForwards.some(fwd => Number(fwd.userId) === Number(me.id))) {
|
||||
throw new GraphQLError('cannot zap a post for which you are forwarded zaps', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -774,7 +780,7 @@ export default {
|
|||
models.$queryRaw`
|
||||
SELECT
|
||||
item_act(${Number(id)}::INTEGER,
|
||||
${me?.id || ANON_USER_ID}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`,
|
||||
${me?.id || ANON_USER_ID}::INTEGER, ${act}::"ItemActType", ${Number(sats)}::INTEGER)`,
|
||||
{ me, models, lnd, hash, hmac, enforceFee: sats }
|
||||
)
|
||||
|
||||
|
@ -783,31 +789,9 @@ export default {
|
|||
return {
|
||||
id,
|
||||
sats,
|
||||
act,
|
||||
path: item.path
|
||||
}
|
||||
},
|
||||
dontLikeThis: async (parent, { id, sats = DONT_LIKE_THIS_COST, hash, hmac }, { me, lnd, models }) => {
|
||||
// need to make sure we are logged in
|
||||
if (!me) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||
}
|
||||
|
||||
// disallow self down votes
|
||||
const [item] = await models.$queryRawUnsafe(`
|
||||
${SELECT}
|
||||
FROM "Item"
|
||||
WHERE id = $1 AND "userId" = $2`, Number(id), me.id)
|
||||
if (item) {
|
||||
throw new GraphQLError('cannot downvote your self', { extensions: { code: 'BAD_INPUT' } })
|
||||
}
|
||||
|
||||
await serializeInvoicable(
|
||||
models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER,
|
||||
${me.id}::INTEGER, 'DONT_LIKE_THIS', ${sats}::INTEGER)`,
|
||||
{ me, models, lnd, hash, hmac }
|
||||
)
|
||||
|
||||
return sats
|
||||
}
|
||||
},
|
||||
Item: {
|
||||
|
|
|
@ -21,6 +21,7 @@ export default gql`
|
|||
id: ID!
|
||||
sats: Int!
|
||||
path: String!
|
||||
act: String!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
|
@ -35,8 +36,7 @@ export default gql`
|
|||
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: [ItemForwardInput], hash: String, hmac: String): Item!
|
||||
updateNoteId(id: ID!, noteId: String!): Item!
|
||||
upsertComment(id:ID, text: String!, parentId: ID, hash: String, hmac: String): Item!
|
||||
dontLikeThis(id: ID!, sats: Int, hash: String, hmac: String): Int!
|
||||
act(id: ID!, sats: Int, hash: String, hmac: String): ItemActResult!
|
||||
act(id: ID!, sats: Int, act: String, hash: String, hmac: String): ItemActResult!
|
||||
pollVote(id: ID!, hash: String, hmac: String): ID!
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { gql, useMutation } from '@apollo/client'
|
||||
import Dropdown from 'react-bootstrap/Dropdown'
|
||||
import { useShowModal } from './modal'
|
||||
import { useToast } from './toast'
|
||||
|
@ -7,6 +6,7 @@ import AccordianItem from './accordian-item'
|
|||
import Flag from '../svgs/flag-fill.svg'
|
||||
import { useMemo } from 'react'
|
||||
import getColor from '../lib/rainbow'
|
||||
import { useLightning } from './lightning'
|
||||
|
||||
export function DownZap ({ id, meDontLikeSats, ...props }) {
|
||||
const style = useMemo(() => (meDontLikeSats
|
||||
|
@ -23,24 +23,7 @@ export function DownZap ({ id, meDontLikeSats, ...props }) {
|
|||
function DownZapper ({ id, As, children }) {
|
||||
const toaster = useToast()
|
||||
const showModal = useShowModal()
|
||||
|
||||
const [dontLikeThis] = useMutation(
|
||||
gql`
|
||||
mutation dontLikeThis($id: ID!, $sats: Int, $hash: String, $hmac: String) {
|
||||
dontLikeThis(id: $id, sats: $sats, hash: $hash, hmac: $hmac)
|
||||
}`, {
|
||||
update (cache, { data: { dontLikeThis } }) {
|
||||
cache.modify({
|
||||
id: `Item:${id}`,
|
||||
fields: {
|
||||
meDontLikeSats (existingSats = 0) {
|
||||
return existingSats + dontLikeThis
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
const strike = useLightning()
|
||||
|
||||
return (
|
||||
<As
|
||||
|
@ -51,7 +34,7 @@ function DownZapper ({ id, As, children }) {
|
|||
onClose={() => {
|
||||
onClose()
|
||||
toaster.success('item downzapped')
|
||||
}} itemId={id} act={dontLikeThis} down
|
||||
}} itemId={id} strike={strike} down
|
||||
>
|
||||
<AccordianItem
|
||||
header='what is a downzap?' body={
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Form, Input, SubmitButton } from './form'
|
|||
import { useMe } from './me'
|
||||
import UpBolt from '../svgs/bolt.svg'
|
||||
import { amountSchema } from '../lib/validate'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
|
||||
const defaultTips = [100, 1000, 10000, 100000]
|
||||
|
||||
|
@ -36,7 +37,7 @@ const addCustomTip = (amount) => {
|
|||
window.localStorage.setItem('custom-tips', JSON.stringify(customTips))
|
||||
}
|
||||
|
||||
export default function ItemAct ({ onClose, itemId, act, down, strike, children }) {
|
||||
export default function ItemAct ({ onClose, itemId, down, strike, children }) {
|
||||
const inputRef = useRef(null)
|
||||
const me = useMe()
|
||||
const [oValue, setOValue] = useState()
|
||||
|
@ -45,6 +46,8 @@ export default function ItemAct ({ onClose, itemId, act, down, strike, children
|
|||
inputRef.current?.focus()
|
||||
}, [onClose, itemId])
|
||||
|
||||
const [act] = useAct()
|
||||
|
||||
const onSubmit = useCallback(async ({ amount, hash, hmac }) => {
|
||||
if (!me) {
|
||||
const storageKey = `TIP-item:${itemId}`
|
||||
|
@ -55,6 +58,7 @@ export default function ItemAct ({ onClose, itemId, act, down, strike, children
|
|||
variables: {
|
||||
id: itemId,
|
||||
sats: Number(amount),
|
||||
act: down ? 'DONT_LIKE_THIS' : 'TIP',
|
||||
hash,
|
||||
hmac
|
||||
}
|
||||
|
@ -62,7 +66,7 @@ export default function ItemAct ({ onClose, itemId, act, down, strike, children
|
|||
strike && await strike()
|
||||
addCustomTip(Number(amount))
|
||||
onClose()
|
||||
}, [act])
|
||||
}, [act, down, itemId, strike])
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
@ -94,3 +98,71 @@ export default function ItemAct ({ onClose, itemId, act, down, strike, children
|
|||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export function useAct ({ onUpdate } = {}) {
|
||||
const me = useMe()
|
||||
|
||||
const update = useCallback((cache, args) => {
|
||||
const { data: { act: { id, sats, path, act } } } = args
|
||||
|
||||
cache.modify({
|
||||
id: `Item:${id}`,
|
||||
fields: {
|
||||
sats (existingSats = 0) {
|
||||
if (act === 'TIP') {
|
||||
return existingSats + sats
|
||||
}
|
||||
|
||||
return existingSats
|
||||
},
|
||||
meSats: me
|
||||
? (existingSats = 0) => {
|
||||
if (act === 'TIP') {
|
||||
return existingSats + sats
|
||||
}
|
||||
|
||||
return existingSats
|
||||
}
|
||||
: undefined,
|
||||
meDontLikeSats: me
|
||||
? (existingSats = 0) => {
|
||||
if (act === 'DONT_LIKE_THIS') {
|
||||
return existingSats + sats
|
||||
}
|
||||
|
||||
return existingSats
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
|
||||
if (act === 'TIP') {
|
||||
// update all ancestors
|
||||
path.split('.').forEach(aId => {
|
||||
if (Number(aId) === Number(id)) return
|
||||
cache.modify({
|
||||
id: `Item:${aId}`,
|
||||
fields: {
|
||||
commentSats (existingCommentSats = 0) {
|
||||
return existingCommentSats + sats
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUpdate && onUpdate(cache, args)
|
||||
}
|
||||
}, [!!me, onUpdate])
|
||||
|
||||
return useMutation(
|
||||
gql`
|
||||
mutation act($id: ID!, $sats: Int!, $act: String, $hash: String, $hmac: String) {
|
||||
act(id: $id, sats: $sats, act: $act, hash: $hash, hmac: $hmac) {
|
||||
id
|
||||
sats
|
||||
path
|
||||
act
|
||||
}
|
||||
}`, { update }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ export default function Items ({ ssrData, variables = {}, query, destructureData
|
|||
}
|
||||
|
||||
export function ListItem ({ item, ...props }) {
|
||||
console.log(item)
|
||||
return (
|
||||
item.parentId
|
||||
? <CommentFlat item={item} noReply includeParent clickToContext {...props} />
|
||||
|
|
|
@ -1,83 +1,51 @@
|
|||
import React from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import Button from 'react-bootstrap/Button'
|
||||
import styles from './pay-bounty.module.css'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import { useMutation, gql } from '@apollo/client'
|
||||
import { useMe } from './me'
|
||||
import { numWithUnits } from '../lib/format'
|
||||
import { useShowModal } from './modal'
|
||||
import { useRoot } from './root'
|
||||
import { payOrLoginError, useInvoiceModal } from './invoice'
|
||||
import { useAct } from './item-act'
|
||||
|
||||
export default function PayBounty ({ children, item }) {
|
||||
const me = useMe()
|
||||
const showModal = useShowModal()
|
||||
const root = useRoot()
|
||||
|
||||
const [act] = useMutation(
|
||||
gql`
|
||||
mutation act($id: ID!, $sats: Int!, $hash: String, $hmac: String) {
|
||||
act(id: $id, sats: $sats, hash: $hash, hmac: $hmac) {
|
||||
sats
|
||||
const onUpdate = useCallback((cache, { data: { act: { id, path } } }) => {
|
||||
// update root bounty status
|
||||
const root = path.split('.')[0]
|
||||
cache.modify({
|
||||
id: `Item:${root}`,
|
||||
fields: {
|
||||
bountyPaidTo (existingPaidTo = []) {
|
||||
return [...(existingPaidTo || []), Number(id)]
|
||||
}
|
||||
}`, {
|
||||
update (cache, { data: { act: { sats } } }) {
|
||||
cache.modify({
|
||||
id: `Item:${item.id}`,
|
||||
fields: {
|
||||
sats (existingSats = 0) {
|
||||
return existingSats + sats
|
||||
},
|
||||
meSats (existingSats = 0) {
|
||||
return existingSats + sats
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// update all ancestor comment sats
|
||||
item.path.split('.').forEach(id => {
|
||||
if (Number(id) === Number(item.id)) return
|
||||
cache.modify({
|
||||
id: `Item:${id}`,
|
||||
fields: {
|
||||
commentSats (existingCommentSats = 0) {
|
||||
return existingCommentSats + sats
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// update root bounty status
|
||||
cache.modify({
|
||||
id: `Item:${root.id}`,
|
||||
fields: {
|
||||
bountyPaidTo (existingPaidTo = []) {
|
||||
return [...(existingPaidTo || []), Number(item.id)]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const [act] = useAct({ onUpdate })
|
||||
|
||||
const showInvoiceModal = useInvoiceModal(async ({ hash, hmac }, { variables }) => {
|
||||
await act({ variables: { ...variables, hash, hmac } })
|
||||
}, [act])
|
||||
|
||||
const handlePayBounty = async onComplete => {
|
||||
const variables = { id: item.id, sats: root.bounty, act: 'TIP', path: item.path }
|
||||
try {
|
||||
await act({
|
||||
variables: { id: item.id, sats: root.bounty },
|
||||
variables,
|
||||
optimisticResponse: {
|
||||
act: {
|
||||
id: `Item:${item.id}`,
|
||||
sats: root.bounty
|
||||
}
|
||||
act: variables
|
||||
}
|
||||
})
|
||||
onComplete()
|
||||
} catch (error) {
|
||||
if (payOrLoginError(error)) {
|
||||
showInvoiceModal({ amount: root.bounty }, { variables: { id: item.id, sats: root.bounty } })
|
||||
showInvoiceModal({ amount: root.bounty }, { variables })
|
||||
return
|
||||
}
|
||||
throw new Error({ message: error.toString() })
|
||||
|
|
|
@ -2,7 +2,7 @@ import UpBolt from '../svgs/bolt.svg'
|
|||
import styles from './upvote.module.css'
|
||||
import { gql, useMutation } from '@apollo/client'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import ItemAct from './item-act'
|
||||
import ItemAct, { useAct } from './item-act'
|
||||
import { useMe } from './me'
|
||||
import getColor from '../lib/rainbow'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
|
@ -56,60 +56,15 @@ const TipPopover = ({ target, show, handleClose }) => (
|
|||
</Overlay>
|
||||
)
|
||||
|
||||
function useAct () {
|
||||
const me = useMe()
|
||||
|
||||
const update = useCallback((cache, { data: { act: { id, sats, path } } }) => {
|
||||
cache.modify({
|
||||
id: `Item:${id}`,
|
||||
fields: {
|
||||
sats (existingSats = 0) {
|
||||
return existingSats + sats
|
||||
},
|
||||
meSats: me
|
||||
? (existingSats = 0) => {
|
||||
return existingSats + sats
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
|
||||
// update all ancestors
|
||||
path.split('.').forEach(aId => {
|
||||
if (Number(aId) === Number(id)) return
|
||||
cache.modify({
|
||||
id: `Item:${aId}`,
|
||||
fields: {
|
||||
commentSats (existingCommentSats = 0) {
|
||||
return existingCommentSats + sats
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}, [!!me])
|
||||
|
||||
return useMutation(
|
||||
gql`
|
||||
mutation act($id: ID!, $sats: Int!, $hash: String, $hmac: String) {
|
||||
act(id: $id, sats: $sats, hash: $hash, hmac: $hmac) {
|
||||
id
|
||||
sats
|
||||
path
|
||||
}
|
||||
}`, { update }
|
||||
)
|
||||
}
|
||||
|
||||
export function DropdownItemUpVote ({ item }) {
|
||||
const showModal = useShowModal()
|
||||
|
||||
const [act] = useAct({ item })
|
||||
const strike = useLightning()
|
||||
|
||||
return (
|
||||
<Dropdown.Item
|
||||
onClick={async () => {
|
||||
showModal(onClose =>
|
||||
<ItemAct onClose={onClose} itemId={item.id} act={act} />)
|
||||
<ItemAct onClose={onClose} itemId={item.id} strike={strike} />)
|
||||
}}
|
||||
>
|
||||
<span className='text-success'>zap</span>
|
||||
|
@ -145,7 +100,6 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
setWalkthrough({ variables: { upvotePopover: true } })
|
||||
}
|
||||
}, [me, voteShow, setWalkthrough])
|
||||
|
||||
const setTipShow = useCallback((yes) => {
|
||||
if (!me) return
|
||||
|
||||
|
@ -161,7 +115,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
}
|
||||
}, [me, tipShow, setWalkthrough])
|
||||
|
||||
const [act] = useAct({ item, setVoteShow, setTipShow })
|
||||
const [act] = useAct()
|
||||
|
||||
const showInvoiceModal = useInvoiceModal(
|
||||
async ({ hash, hmac }, { variables }) => {
|
||||
|
@ -171,7 +125,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
|
||||
const zap = useDebounceCallback(async (sats) => {
|
||||
if (!sats) return
|
||||
const variables = { id: item.id, sats }
|
||||
const variables = { id: item.id, sats, act: 'TIP' }
|
||||
|
||||
act({
|
||||
variables,
|
||||
|
@ -179,7 +133,8 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
act: {
|
||||
id: item.id,
|
||||
sats,
|
||||
path: item.path
|
||||
path: item.path,
|
||||
act: 'TIP'
|
||||
}
|
||||
}
|
||||
}).catch((error) => {
|
||||
|
@ -229,7 +184,7 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
|
||||
setTipShow(false)
|
||||
showModal(onClose =>
|
||||
<ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
|
||||
<ItemAct onClose={onClose} itemId={item.id} strike={strike} />)
|
||||
}
|
||||
}
|
||||
onShortPress={
|
||||
|
@ -244,6 +199,8 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
|
||||
if (meSats) {
|
||||
setVoteShow(false)
|
||||
} else {
|
||||
setTipShow(true)
|
||||
}
|
||||
|
||||
strike()
|
||||
|
|
|
@ -341,6 +341,11 @@ export const amountSchema = object({
|
|||
amount: intValidator.required('required').positive('must be positive')
|
||||
})
|
||||
|
||||
export const actSchema = object({
|
||||
sats: intValidator.required('required').positive('must be positive'),
|
||||
act: string().required('required').oneOf(['TIP', 'DONT_LIKE_THIS'])
|
||||
})
|
||||
|
||||
export const settingsSchema = object({
|
||||
tipDefault: intValidator.required('required').positive('must be positive'),
|
||||
fiatCurrency: string().required('required').oneOf(SUPPORTED_CURRENCIES),
|
||||
|
|
Loading…
Reference in New Issue