commit
33325f7c17
|
@ -290,7 +290,7 @@ export default {
|
|||
if (payer) {
|
||||
payer = {
|
||||
...payer,
|
||||
identifier: payer.identifier ? me.name : undefined
|
||||
identifier: payer.identifier ? `${me.name}@stacker.news` : undefined
|
||||
}
|
||||
payer = Object.fromEntries(
|
||||
Object.entries(payer).filter(([, value]) => !!value)
|
||||
|
@ -305,10 +305,10 @@ export default {
|
|||
callback.searchParams.append('comment', comment)
|
||||
}
|
||||
|
||||
let encodedPayerData = ''
|
||||
let stringifiedPayerData = ''
|
||||
if (payer && Object.entries(payer).length) {
|
||||
encodedPayerData = encodeURIComponent(JSON.stringify(payer))
|
||||
callback.searchParams.append('payerdata', encodedPayerData)
|
||||
stringifiedPayerData = JSON.stringify(payer)
|
||||
callback.searchParams.append('payerdata', stringifiedPayerData)
|
||||
}
|
||||
|
||||
// call callback with amount and conditionally comment
|
||||
|
@ -320,7 +320,7 @@ export default {
|
|||
// decode invoice
|
||||
try {
|
||||
const decoded = await decodePaymentRequest({ lnd, request: res.pr })
|
||||
if (decoded.description_hash !== lnurlPayDescriptionHash(`${options.metadata}${encodedPayerData}`)) {
|
||||
if (decoded.description_hash !== lnurlPayDescriptionHash(`${options.metadata}${stringifiedPayerData}`)) {
|
||||
throw new Error('description hash does not match')
|
||||
}
|
||||
if (!decoded.mtokens || BigInt(decoded.mtokens) !== BigInt(milliamount)) {
|
||||
|
|
|
@ -2,8 +2,6 @@ import { gql, useMutation } from '@apollo/client'
|
|||
import Dropdown from 'react-bootstrap/Dropdown'
|
||||
import { useShowModal } from './modal'
|
||||
import { useToast } from './toast'
|
||||
import { InvoiceModal, payOrLoginError } from './invoice'
|
||||
import { DONT_LIKE_THIS_COST } from '../lib/constants'
|
||||
import ItemAct from './item-act'
|
||||
|
||||
export default function DontLikeThisDropdownItem ({ id }) {
|
||||
|
@ -40,22 +38,7 @@ export default function DontLikeThisDropdownItem ({ id }) {
|
|||
}} itemId={id} act={dontLikeThis} down
|
||||
/>)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
if (payOrLoginError(error)) {
|
||||
showModal(onClose => {
|
||||
return (
|
||||
<InvoiceModal
|
||||
amount={DONT_LIKE_THIS_COST}
|
||||
onPayment={async ({ hash, hmac }) => {
|
||||
await dontLikeThis({ variables: { id, hash, hmac } })
|
||||
toaster.success('item flagged')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
} else {
|
||||
toaster.danger('failed to flag item')
|
||||
}
|
||||
toaster.danger('failed to flag item')
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -24,6 +24,7 @@ import { numWithUnits } from '../lib/format'
|
|||
import textAreaCaret from 'textarea-caret'
|
||||
import ReactDatePicker from 'react-datepicker'
|
||||
import 'react-datepicker/dist/react-datepicker.css'
|
||||
import { debounce } from './use-debounce-callback'
|
||||
|
||||
export function SubmitButton ({
|
||||
children, variant, value, onClick, disabled, cost, ...props
|
||||
|
@ -290,7 +291,7 @@ function FormGroup ({ className, label, children }) {
|
|||
|
||||
function InputInner ({
|
||||
prepend, append, hint, showValid, onChange, onBlur, overrideValue,
|
||||
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce, maxLength,
|
||||
innerRef, noForm, clear, onKeyDown, inputGroupClassName, debounce: debounceTime, maxLength,
|
||||
...props
|
||||
}) {
|
||||
const [field, meta, helpers] = noForm ? [{}, {}, {}] : useField(props)
|
||||
|
@ -317,17 +318,11 @@ function InputInner ({
|
|||
|
||||
const invalid = (!formik || formik.submitCount > 0) && meta.touched && meta.error
|
||||
|
||||
const debounceRef = useRef(-1)
|
||||
|
||||
useEffect(() => {
|
||||
if (debounceRef.current !== -1) {
|
||||
clearTimeout(debounceRef.current)
|
||||
useEffect(debounce(() => {
|
||||
if (!noForm && !isNaN(debounceTime) && debounceTime > 0) {
|
||||
formik.validateForm()
|
||||
}
|
||||
if (!noForm && !isNaN(debounce) && debounce > 0) {
|
||||
debounceRef.current = setTimeout(() => formik.validateForm(), debounce)
|
||||
}
|
||||
return () => clearTimeout(debounceRef.current)
|
||||
}, [noForm, formik, field.value])
|
||||
}, debounceTime), [noForm, formik, field.value])
|
||||
|
||||
const remaining = maxLength && maxLength - (field.value || '').length
|
||||
|
||||
|
@ -601,7 +596,7 @@ export function Form ({
|
|||
const toaster = useToast()
|
||||
const initialErrorToasted = useRef(false)
|
||||
useEffect(() => {
|
||||
if (initialError && !initialErrorToasted) {
|
||||
if (initialError && !initialErrorToasted.current) {
|
||||
toaster.danger(initialError.message || initialError.toString?.())
|
||||
initialErrorToasted.current = true
|
||||
}
|
||||
|
|
|
@ -149,6 +149,9 @@ const defaultOptions = {
|
|||
callback: null, // (formValues) => void
|
||||
replaceModal: false
|
||||
}
|
||||
// TODO: refactor this so it can be easily understood
|
||||
// there's lots of state cascading paired with logic
|
||||
// independent of the state, and it's hard to follow
|
||||
export const useInvoiceable = (onSubmit, options = defaultOptions) => {
|
||||
const me = useMe()
|
||||
const [createInvoice, { data }] = useMutation(gql`
|
||||
|
@ -247,17 +250,14 @@ export const useInvoiceable = (onSubmit, options = defaultOptions) => {
|
|||
// tell onSubmit handler that we want to keep local storage
|
||||
// even though the submit handler was "successful"
|
||||
return { keepLocalStorage: true }
|
||||
}, [onSubmit, setFormValues, setSubmitArgs, createInvoice])
|
||||
}, [onSubmit, setFormValues, setSubmitArgs, createInvoice, !!me])
|
||||
|
||||
return onSubmitWrapper
|
||||
}
|
||||
|
||||
export const InvoiceModal = ({ onPayment, amount }) => {
|
||||
const createInvoice = useInvoiceable(onPayment, { replaceModal: true })
|
||||
|
||||
useEffect(() => {
|
||||
createInvoice({ amount })
|
||||
}, [])
|
||||
export const useInvoiceModal = (onPayment, deps) => {
|
||||
const onPaymentMemo = useCallback(onPayment, deps)
|
||||
return useInvoiceable(onPaymentMemo, { replaceModal: true })
|
||||
}
|
||||
|
||||
export const payOrLoginError = (error) => {
|
||||
|
|
|
@ -67,7 +67,7 @@ export default function ItemAct ({ onClose, itemId, act, down, strike }) {
|
|||
return (
|
||||
<Form
|
||||
initial={{
|
||||
amount: me?.tipDefault,
|
||||
amount: me?.tipDefault || defaultTips[0],
|
||||
default: false
|
||||
}}
|
||||
schema={amountSchema}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useMe } from './me'
|
|||
import { numWithUnits } from '../lib/format'
|
||||
import { useShowModal } from './modal'
|
||||
import { useRoot } from './root'
|
||||
import { InvoiceModal, payOrLoginError } from './invoice'
|
||||
import { payOrLoginError, useInvoiceModal } from './invoice'
|
||||
|
||||
export default function PayBounty ({ children, item }) {
|
||||
const me = useMe()
|
||||
|
@ -59,6 +59,9 @@ export default function PayBounty ({ children, item }) {
|
|||
}
|
||||
}
|
||||
)
|
||||
const showInvoiceModal = useInvoiceModal(async ({ hash, hmac }, { variables }) => {
|
||||
await act({ variables: { ...variables, hash, hmac } })
|
||||
}, [act])
|
||||
|
||||
const handlePayBounty = async onComplete => {
|
||||
try {
|
||||
|
@ -74,16 +77,7 @@ export default function PayBounty ({ children, item }) {
|
|||
onComplete()
|
||||
} catch (error) {
|
||||
if (payOrLoginError(error)) {
|
||||
showModal(onClose => {
|
||||
return (
|
||||
<InvoiceModal
|
||||
amount={root.bounty}
|
||||
onPayment={async ({ hash, hmac }) => {
|
||||
await act({ variables: { id: item.id, sats: root.bounty, hash, hmac } })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
showInvoiceModal({ amount: root.bounty }, { variables: { id: item.id, sats: root.bounty } })
|
||||
return
|
||||
}
|
||||
throw new Error({ message: error.toString() })
|
||||
|
|
|
@ -7,13 +7,11 @@ import styles from './poll.module.css'
|
|||
import Check from '../svgs/checkbox-circle-fill.svg'
|
||||
import { signIn } from 'next-auth/react'
|
||||
import ActionTooltip from './action-tooltip'
|
||||
import { useShowModal } from './modal'
|
||||
import { POLL_COST } from '../lib/constants'
|
||||
import { InvoiceModal } from './invoice'
|
||||
import { payOrLoginError, useInvoiceModal } from './invoice'
|
||||
|
||||
export default function Poll ({ item }) {
|
||||
const me = useMe()
|
||||
const showModal = useShowModal()
|
||||
const [pollVote] = useMutation(
|
||||
gql`
|
||||
mutation pollVote($id: ID!, $hash: String, $hmac: String) {
|
||||
|
@ -47,6 +45,12 @@ export default function Poll ({ item }) {
|
|||
)
|
||||
|
||||
const PollButton = ({ v }) => {
|
||||
const showInvoiceModal = useInvoiceModal(async ({ hash, hmac }, { variables }) => {
|
||||
await pollVote({ variables: { ...variables, hash, hmac } })
|
||||
}, [pollVote])
|
||||
|
||||
const variables = { id: v.id }
|
||||
|
||||
return (
|
||||
<ActionTooltip placement='left' notForm>
|
||||
<Button
|
||||
|
@ -55,22 +59,17 @@ export default function Poll ({ item }) {
|
|||
? async () => {
|
||||
try {
|
||||
await pollVote({
|
||||
variables: { id: v.id },
|
||||
variables,
|
||||
optimisticResponse: {
|
||||
pollVote: v.id
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
showModal(onClose => {
|
||||
return (
|
||||
<InvoiceModal
|
||||
amount={item.pollCost || POLL_COST}
|
||||
onPayment={async ({ hash, hmac }) => {
|
||||
await pollVote({ variables: { id: v.id, hash, hmac } })
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
if (payOrLoginError(error)) {
|
||||
showInvoiceModal({ amount: item.pollCost || POLL_COST }, { variables })
|
||||
return
|
||||
}
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
}
|
||||
: signIn}
|
||||
|
|
|
@ -5,14 +5,15 @@ import ActionTooltip from './action-tooltip'
|
|||
import ItemAct from './item-act'
|
||||
import { useMe } from './me'
|
||||
import Rainbow from '../lib/rainbow'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import LongPressable from 'react-longpressable'
|
||||
import Overlay from 'react-bootstrap/Overlay'
|
||||
import Popover from 'react-bootstrap/Popover'
|
||||
import { useShowModal } from './modal'
|
||||
import { LightningConsumer, useLightning } from './lightning'
|
||||
import { numWithUnits } from '../lib/format'
|
||||
import { InvoiceModal, payOrLoginError } from './invoice'
|
||||
import { payOrLoginError, useInvoiceModal } from './invoice'
|
||||
import useDebounceCallback from './use-debounce-callback'
|
||||
|
||||
const getColor = (meSats) => {
|
||||
if (!meSats || meSats <= 10) {
|
||||
|
@ -69,7 +70,6 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
const [voteShow, _setVoteShow] = useState(false)
|
||||
const [tipShow, _setTipShow] = useState(false)
|
||||
const ref = useRef()
|
||||
const timerRef = useRef(null)
|
||||
const me = useMe()
|
||||
const strike = useLightning()
|
||||
const [setWalkthrough] = useMutation(
|
||||
|
@ -153,55 +153,33 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
}
|
||||
}
|
||||
)
|
||||
const showInvoiceModal = useInvoiceModal(
|
||||
async ({ hash, hmac }, { variables }) => {
|
||||
await act({ variables: { ...variables, hash, hmac } })
|
||||
strike()
|
||||
}, [act, strike])
|
||||
|
||||
// if we want to use optimistic response, we need to buffer the votes
|
||||
// because if someone votes in quick succession, responses come back out of order
|
||||
// so we wait a bit to see if there are more votes coming in
|
||||
// this effectively performs our own debounced optimistic response
|
||||
useEffect(() => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
|
||||
if (pendingSats > 0) {
|
||||
timerRef.current = setTimeout(async (sats) => {
|
||||
const variables = { id: item.id, sats: pendingSats }
|
||||
try {
|
||||
timerRef.current && setPendingSats(0)
|
||||
await act({
|
||||
variables,
|
||||
optimisticResponse: {
|
||||
act: {
|
||||
sats
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
if (payOrLoginError(error)) {
|
||||
showModal(onClose => {
|
||||
return (
|
||||
<InvoiceModal
|
||||
amount={pendingSats}
|
||||
onPayment={async ({ hash, hmac }) => {
|
||||
await act({ variables: { ...variables, hash, hmac } })
|
||||
strike()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return
|
||||
const zap = useDebounceCallback(async (sats) => {
|
||||
if (!sats) return
|
||||
const variables = { id: item.id, sats }
|
||||
try {
|
||||
setPendingSats(0)
|
||||
await act({
|
||||
variables,
|
||||
optimisticResponse: {
|
||||
act: {
|
||||
sats
|
||||
}
|
||||
if (!timerRef.current) return
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
}, 500, pendingSats)
|
||||
})
|
||||
} catch (error) {
|
||||
if (payOrLoginError(error)) {
|
||||
showInvoiceModal({ amount: sats }, { variables })
|
||||
return
|
||||
}
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
|
||||
return async () => {
|
||||
clearTimeout(timerRef.current)
|
||||
timerRef.current = null
|
||||
}
|
||||
}, [pendingSats, act, item, showModal, setPendingSats])
|
||||
}, 500, [act, item?.id, showInvoiceModal, setPendingSats])
|
||||
|
||||
const disabled = useMemo(() => item?.mine || item?.meForward || item?.deletedAt,
|
||||
[item?.mine, item?.meForward, item?.deletedAt])
|
||||
|
@ -258,7 +236,11 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
|
||||
strike()
|
||||
|
||||
setPendingSats(pendingSats + sats)
|
||||
setPendingSats(pendingSats => {
|
||||
const zapAmount = pendingSats + sats
|
||||
zap(zapAmount)
|
||||
return zapAmount
|
||||
})
|
||||
}
|
||||
: () => showModal(onClose => <ItemAct onClose={onClose} itemId={item.id} act={act} strike={strike} />)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
import useNoInitialEffect from './use-no-initial-effect'
|
||||
|
||||
export function debounce (fn, time) {
|
||||
let timeoutId
|
||||
return wrapper
|
||||
function wrapper (...args) {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = null
|
||||
fn(...args)
|
||||
}, time)
|
||||
// return a function that clears the timeout for use in useEffect cleanup
|
||||
return () => clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
|
||||
export default function useDebounceCallback (fn, time, deps = []) {
|
||||
const [args, setArgs] = useState([])
|
||||
const memoFn = useCallback(fn, deps)
|
||||
useNoInitialEffect(debounce(() => memoFn(...args), time), [memoFn, time, args, ...deps])
|
||||
return useCallback((...args) => setArgs(args), [])
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
|
||||
export default function useNoInitialEffect (func, deps) {
|
||||
const didMount = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (didMount.current) {
|
||||
return func()
|
||||
} else {
|
||||
didMount.current = true
|
||||
}
|
||||
}, deps)
|
||||
}
|
|
@ -50,7 +50,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
|
|||
let parsedPayerData
|
||||
if (payerData) {
|
||||
try {
|
||||
parsedPayerData = JSON.parse(decodeURIComponent(payerData))
|
||||
parsedPayerData = JSON.parse(payerData)
|
||||
} catch (err) {
|
||||
console.error('failed to parse payerdata', err)
|
||||
return res.status(400).json({ status: 'ERROR', reason: 'Invalid JSON supplied for payerdata parameter' })
|
||||
|
|
|
@ -21,6 +21,7 @@ import styles from '../components/user-header.module.css'
|
|||
import HiddenWalletSummary from '../components/hidden-wallet-summary'
|
||||
import AccordianItem from '../components/accordian-item'
|
||||
import { lnAddrOptions } from '../lib/lnurl'
|
||||
import useDebounceCallback from '../components/use-debounce-callback'
|
||||
|
||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||
|
||||
|
@ -300,7 +301,13 @@ export function LnAddrWithdrawal () {
|
|||
const [addrOptions, setAddrOptions] = useState(defaultOptions)
|
||||
const [formSchema, setFormSchema] = useState(lnAddrSchema())
|
||||
|
||||
const onAddrChange = async (formik, e) => {
|
||||
const onAddrChange = useDebounceCallback(async (formik, e) => {
|
||||
if (!e?.target?.value) {
|
||||
setAddrOptions(defaultOptions)
|
||||
setFormSchema(lnAddrSchema())
|
||||
return
|
||||
}
|
||||
|
||||
let options
|
||||
try {
|
||||
options = await lnAddrOptions(e.target.value)
|
||||
|
@ -312,7 +319,7 @@ export function LnAddrWithdrawal () {
|
|||
|
||||
setAddrOptions(options)
|
||||
setFormSchema(lnAddrSchema(options))
|
||||
}
|
||||
}, 500, [setAddrOptions, setFormSchema])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
Loading…
Reference in New Issue