UX latency enhancements for paid actions (#1434)
* prevent multiple retries & pulse retry button * fix lint * don't wait for settlement on pessimistic zaps * optimistic act modal
This commit is contained in:
parent
450c969dfc
commit
9f06fd65ee
|
@ -160,7 +160,7 @@ async function performPessimisticAction (actionType, args, context) {
|
||||||
|
|
||||||
export async function retryPaidAction (actionType, args, context) {
|
export async function retryPaidAction (actionType, args, context) {
|
||||||
const { models, me } = context
|
const { models, me } = context
|
||||||
const { invoiceId } = args
|
const { invoice: failedInvoice } = args
|
||||||
|
|
||||||
console.log('retryPaidAction', actionType, args)
|
console.log('retryPaidAction', actionType, args)
|
||||||
|
|
||||||
|
@ -181,18 +181,13 @@ export async function retryPaidAction (actionType, args, context) {
|
||||||
throw new Error(`retryPaidAction - action does not support retrying ${actionType}`)
|
throw new Error(`retryPaidAction - action does not support retrying ${actionType}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invoiceId) {
|
if (!failedInvoice) {
|
||||||
throw new Error(`retryPaidAction - missing invoiceId ${actionType}`)
|
throw new Error(`retryPaidAction - missing invoice ${actionType}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
context.optimistic = true
|
context.optimistic = true
|
||||||
context.me = await models.user.findUnique({ where: { id: me.id } })
|
context.me = await models.user.findUnique({ where: { id: me.id } })
|
||||||
|
|
||||||
const failedInvoice = await models.invoice.findUnique({ where: { id: invoiceId, actionState: 'FAILED' } })
|
|
||||||
if (!failedInvoice) {
|
|
||||||
throw new Error(`retryPaidAction ${actionType} - invoice ${invoiceId} not found or not in failed state`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { msatsRequested, actionId } = failedInvoice
|
const { msatsRequested, actionId } = failedInvoice
|
||||||
context.cost = BigInt(msatsRequested)
|
context.cost = BigInt(msatsRequested)
|
||||||
context.actionId = actionId
|
context.actionId = actionId
|
||||||
|
@ -204,7 +199,7 @@ export async function retryPaidAction (actionType, args, context) {
|
||||||
// update the old invoice to RETRYING, so that it's not confused with FAILED
|
// update the old invoice to RETRYING, so that it's not confused with FAILED
|
||||||
await tx.invoice.update({
|
await tx.invoice.update({
|
||||||
where: {
|
where: {
|
||||||
id: invoiceId,
|
id: failedInvoice.id,
|
||||||
actionState: 'FAILED'
|
actionState: 'FAILED'
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
@ -216,7 +211,7 @@ export async function retryPaidAction (actionType, args, context) {
|
||||||
const invoice = await createDbInvoice(actionType, args, context, invoiceArgs)
|
const invoice = await createDbInvoice(actionType, args, context, invoiceArgs)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: await action.retry({ invoiceId, newInvoiceId: invoice.id }, context),
|
result: await action.retry({ invoiceId: failedInvoice.id, newInvoiceId: invoice.id }, context),
|
||||||
invoice,
|
invoice,
|
||||||
paymentMethod: 'OPTIMISTIC'
|
paymentMethod: 'OPTIMISTIC'
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,14 @@ export default {
|
||||||
throw new Error('Invoice not found')
|
throw new Error('Invoice not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await retryPaidAction(invoice.actionType, { invoiceId }, { models, me, lnd })
|
if (invoice.actionState !== 'FAILED') {
|
||||||
|
if (invoice.actionState === 'PAID') {
|
||||||
|
throw new Error('Invoice is already paid')
|
||||||
|
}
|
||||||
|
throw new Error(`Invoice is not in failed state: ${invoice.actionState}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await retryPaidAction(invoice.actionType, { invoice }, { models, me, lnd })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
|
|
|
@ -94,7 +94,7 @@ export function CommentFlat ({ item, rank, siblingComments, ...props }) {
|
||||||
|
|
||||||
export default function Comment ({
|
export default function Comment ({
|
||||||
item, children, replyOpen, includeParent, topLevel,
|
item, children, replyOpen, includeParent, topLevel,
|
||||||
rootText, noComments, noReply, truncate, depth, pin
|
rootText, noComments, noReply, truncate, depth, pin, setDisableRetry, disableRetry
|
||||||
}) {
|
}) {
|
||||||
const [edit, setEdit] = useState()
|
const [edit, setEdit] = useState()
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
|
@ -169,6 +169,8 @@ export default function Comment ({
|
||||||
embellishUser={op && <><span> </span><Badge bg={op === 'fwd' ? 'secondary' : 'boost'} className={`${styles.op} bg-opacity-75`}>{op}</Badge></>}
|
embellishUser={op && <><span> </span><Badge bg={op === 'fwd' ? 'secondary' : 'boost'} className={`${styles.op} bg-opacity-75`}>{op}</Badge></>}
|
||||||
onQuoteReply={quoteReply}
|
onQuoteReply={quoteReply}
|
||||||
nested={!includeParent}
|
nested={!includeParent}
|
||||||
|
setDisableRetry={setDisableRetry}
|
||||||
|
disableRetry={disableRetry}
|
||||||
extraInfo={
|
extraInfo={
|
||||||
<>
|
<>
|
||||||
{includeParent && <Parent item={item} rootText={rootText} />}
|
{includeParent && <Parent item={item} rootText={rootText} />}
|
||||||
|
|
|
@ -53,7 +53,7 @@ export function SubmitButton ({
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant={variant || 'main'}
|
variant={variant || 'main'}
|
||||||
className={classNames(formik.isSubmitting && styles.pending, className)}
|
className={classNames(formik.isSubmitting && 'pulse', className)}
|
||||||
type='submit'
|
type='submit'
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={value
|
onClick={value
|
||||||
|
|
|
@ -13,8 +13,12 @@ import ItemJob from './item-job'
|
||||||
import Item from './item'
|
import Item from './item'
|
||||||
import { CommentFlat } from './comment'
|
import { CommentFlat } from './comment'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
|
import Moon from '@/svgs/moon-fill.svg'
|
||||||
|
|
||||||
export default function Invoice ({ id, query = INVOICE, modal, onPayment, onCanceled, info, successVerb, useWallet = true, walletError, poll, waitFor, ...props }) {
|
export default function Invoice ({
|
||||||
|
id, query = INVOICE, modal, onPayment, onCanceled, info, successVerb = 'deposited',
|
||||||
|
heldVerb = 'settling', useWallet = true, walletError, poll, waitFor, ...props
|
||||||
|
}) {
|
||||||
const [expired, setExpired] = useState(false)
|
const [expired, setExpired] = useState(false)
|
||||||
const { data, error } = useQuery(query, SSR
|
const { data, error } = useQuery(query, SSR
|
||||||
? {}
|
? {}
|
||||||
|
@ -55,9 +59,17 @@ export default function Invoice ({ id, query = INVOICE, modal, onPayment, onCanc
|
||||||
variant = 'failed'
|
variant = 'failed'
|
||||||
status = 'cancelled'
|
status = 'cancelled'
|
||||||
useWallet = false
|
useWallet = false
|
||||||
} else if (invoice.confirmedAt || (invoice.isHeld && invoice.satsReceived && !expired)) {
|
} else if (invoice.isHeld && invoice.satsReceived && !expired) {
|
||||||
|
variant = 'pending'
|
||||||
|
status = (
|
||||||
|
<div className='d-flex justify-content-center'>
|
||||||
|
<Moon className='spin fill-grey me-2' /> {heldVerb}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
useWallet = false
|
||||||
|
} else if (invoice.confirmedAt) {
|
||||||
variant = 'confirmed'
|
variant = 'confirmed'
|
||||||
status = `${numWithUnits(invoice.satsReceived, { abbreviate: false })} ${successVerb || 'deposited'}`
|
status = `${numWithUnits(invoice.satsReceived, { abbreviate: false })} ${successVerb}`
|
||||||
useWallet = false
|
useWallet = false
|
||||||
} else if (expired) {
|
} else if (expired) {
|
||||||
variant = 'failed'
|
variant = 'failed'
|
||||||
|
|
|
@ -90,6 +90,7 @@ function BoostForm ({ step, onSubmit, children, item, oValue, inputRef, act = 'B
|
||||||
export default function ItemAct ({ onClose, item, act = 'TIP', step, children, abortSignal }) {
|
export default function ItemAct ({ onClose, item, act = 'TIP', step, children, abortSignal }) {
|
||||||
const inputRef = useRef(null)
|
const inputRef = useRef(null)
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
|
const wallet = useWallet()
|
||||||
const [oValue, setOValue] = useState()
|
const [oValue, setOValue] = useState()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -110,6 +111,18 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onPaid = () => {
|
||||||
|
strike()
|
||||||
|
onClose?.()
|
||||||
|
if (!me) setItemMeAnonSats({ id: item.id, amount })
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeImmediately = !!wallet || me?.privates?.sats > Number(amount)
|
||||||
|
if (closeImmediately) {
|
||||||
|
onPaid()
|
||||||
|
}
|
||||||
|
|
||||||
const { error } = await actor({
|
const { error } = await actor({
|
||||||
variables: {
|
variables: {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
@ -127,15 +140,11 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
// don't close modal immediately because we want the QR modal to stack
|
// don't close modal immediately because we want the QR modal to stack
|
||||||
onCompleted: () => {
|
onPaid: closeImmediately ? undefined : onPaid
|
||||||
strike()
|
|
||||||
onClose?.()
|
|
||||||
if (!me) setItemMeAnonSats({ id: item.id, amount })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
if (error) throw error
|
if (error) throw error
|
||||||
addCustomTip(Number(amount))
|
addCustomTip(Number(amount))
|
||||||
}, [me, actor, act, item.id, onClose, abortSignal, strike])
|
}, [me, actor, !!wallet, act, item.id, onClose, abortSignal, strike])
|
||||||
|
|
||||||
return act === 'BOOST'
|
return act === 'BOOST'
|
||||||
? <BoostForm step={step} onSubmit={onSubmit} item={item} oValue={oValue} inputRef={inputRef} act={act}>{children}</BoostForm>
|
? <BoostForm step={step} onSubmit={onSubmit} item={item} oValue={oValue} inputRef={inputRef} act={act}>{children}</BoostForm>
|
||||||
|
@ -226,6 +235,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
|
||||||
const getPaidActionResult = data => Object.values(data)[0]
|
const getPaidActionResult = data => Object.values(data)[0]
|
||||||
|
|
||||||
const [act] = usePaidMutation(query, {
|
const [act] = usePaidMutation(query, {
|
||||||
|
waitFor: inv => inv?.satsReceived > 0,
|
||||||
...options,
|
...options,
|
||||||
update: (cache, { data }) => {
|
update: (cache, { data }) => {
|
||||||
const response = getPaidActionResult(data)
|
const response = getPaidActionResult(data)
|
||||||
|
|
|
@ -26,20 +26,20 @@ import { useQrPayment } from './payment'
|
||||||
import { useRetryCreateItem } from './use-item-submit'
|
import { useRetryCreateItem } from './use-item-submit'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
export default function ItemInfo ({
|
export default function ItemInfo ({
|
||||||
item, full, commentsText = 'comments',
|
item, full, commentsText = 'comments',
|
||||||
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
commentTextSingular = 'comment', className, embellishUser, extraInfo, onEdit, editText,
|
||||||
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true
|
onQuoteReply, extraBadges, nested, pinnable, showActionDropdown = true, showUser = true,
|
||||||
|
setDisableRetry, disableRetry
|
||||||
}) {
|
}) {
|
||||||
const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000
|
const editThreshold = new Date(item.invoice?.confirmedAt ?? item.createdAt).getTime() + 10 * 60000
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
const toaster = useToast()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [canEdit, setCanEdit] = useState(item.mine && (Date.now() < editThreshold))
|
const [canEdit, setCanEdit] = useState(item.mine && (Date.now() < editThreshold))
|
||||||
const [hasNewComments, setHasNewComments] = useState(false)
|
const [hasNewComments, setHasNewComments] = useState(false)
|
||||||
const root = useRoot()
|
const root = useRoot()
|
||||||
const retryCreateItem = useRetryCreateItem({ id: item.id })
|
|
||||||
const sub = item?.sub || root?.sub
|
const sub = item?.sub || root?.sub
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -64,72 +64,6 @@ export default function ItemInfo ({
|
||||||
const canPin = (isPost && mySub) || (myPost && rootReply)
|
const canPin = (isPost && mySub) || (myPost && rootReply)
|
||||||
const meSats = (me ? item.meSats : item.meAnonSats) || 0
|
const meSats = (me ? item.meSats : item.meAnonSats) || 0
|
||||||
|
|
||||||
const EditInfo = () => {
|
|
||||||
if (canEdit) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span> \ </span>
|
|
||||||
<span
|
|
||||||
className='text-reset pointer fw-bold'
|
|
||||||
onClick={() => onEdit ? onEdit() : router.push(`/items/${item.id}/edit`)}
|
|
||||||
>
|
|
||||||
<span>{editText || 'edit'} </span>
|
|
||||||
{(!item.invoice?.actionState || item.invoice?.actionState === 'PAID') &&
|
|
||||||
<Countdown
|
|
||||||
date={editThreshold}
|
|
||||||
onComplete={() => { setCanEdit(false) }}
|
|
||||||
/>}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const PaymentInfo = () => {
|
|
||||||
const waitForQrPayment = useQrPayment()
|
|
||||||
if (item.deletedAt) return null
|
|
||||||
|
|
||||||
let Component
|
|
||||||
let onClick
|
|
||||||
if (me && item.invoice?.actionState && item.invoice?.actionState !== 'PAID') {
|
|
||||||
if (item.invoice?.actionState === 'FAILED') {
|
|
||||||
Component = () => <span className='text-warning'>retry payment</span>
|
|
||||||
onClick = async () => {
|
|
||||||
try {
|
|
||||||
const { error } = await retryCreateItem({ variables: { invoiceId: parseInt(item.invoice?.id) } })
|
|
||||||
if (error) throw error
|
|
||||||
} catch (error) {
|
|
||||||
toaster.danger(error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Component = () => (
|
|
||||||
<span
|
|
||||||
className='text-info'
|
|
||||||
>pending
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
onClick = () => waitForQrPayment({ id: item.invoice?.id }, null, { cancelOnClose: false }).catch(console.error)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span> \ </span>
|
|
||||||
<span
|
|
||||||
className='text-reset pointer fw-bold'
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<Component />
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className || `${styles.other}`}>
|
<div className={className || `${styles.other}`}>
|
||||||
{!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === USER_ID.ad) &&
|
{!(item.position && (pinnable || !item.subName)) && !(!item.parentId && Number(item.user?.id) === USER_ID.ad) &&
|
||||||
|
@ -217,8 +151,11 @@ export default function ItemInfo ({
|
||||||
{
|
{
|
||||||
showActionDropdown &&
|
showActionDropdown &&
|
||||||
<>
|
<>
|
||||||
<EditInfo />
|
<EditInfo
|
||||||
<PaymentInfo />
|
item={item} canEdit={canEdit}
|
||||||
|
setCanEdit={setCanEdit} onEdit={onEdit} editText={editText} editThreshold={editThreshold}
|
||||||
|
/>
|
||||||
|
<PaymentInfo item={item} disableRetry={disableRetry} setDisableRetry={setDisableRetry} />
|
||||||
<ActionDropdown>
|
<ActionDropdown>
|
||||||
<CopyLinkDropdownItem item={item} />
|
<CopyLinkDropdownItem item={item} />
|
||||||
<InfoDropdownItem item={item} />
|
<InfoDropdownItem item={item} />
|
||||||
|
@ -317,3 +254,85 @@ function InfoDropdownItem ({ item }) {
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PaymentInfo ({ item, disableRetry, setDisableRetry }) {
|
||||||
|
const { me } = useMe()
|
||||||
|
const toaster = useToast()
|
||||||
|
const retryCreateItem = useRetryCreateItem({ id: item.id })
|
||||||
|
const waitForQrPayment = useQrPayment()
|
||||||
|
const [disableInfoRetry, setDisableInfoRetry] = useState(disableRetry)
|
||||||
|
if (item.deletedAt) return null
|
||||||
|
|
||||||
|
const disableDualRetry = disableRetry || disableInfoRetry
|
||||||
|
function setDisableDualRetry (value) {
|
||||||
|
setDisableInfoRetry(value)
|
||||||
|
setDisableRetry?.(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
let Component
|
||||||
|
let onClick
|
||||||
|
if (me && item.invoice?.actionState && item.invoice?.actionState !== 'PAID') {
|
||||||
|
if (item.invoice?.actionState === 'FAILED') {
|
||||||
|
Component = () => <span className={classNames('text-warning', disableDualRetry && 'pulse')}>retry payment</span>
|
||||||
|
onClick = async () => {
|
||||||
|
if (disableDualRetry) return
|
||||||
|
setDisableDualRetry(true)
|
||||||
|
try {
|
||||||
|
const { error } = await retryCreateItem({ variables: { invoiceId: parseInt(item.invoice?.id) } })
|
||||||
|
if (error) throw error
|
||||||
|
} catch (error) {
|
||||||
|
toaster.danger(error.message)
|
||||||
|
} finally {
|
||||||
|
setDisableDualRetry(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Component = () => (
|
||||||
|
<span
|
||||||
|
className='text-info'
|
||||||
|
>pending
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
onClick = () => waitForQrPayment({ id: item.invoice?.id }, null, { cancelOnClose: false }).catch(console.error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span> \ </span>
|
||||||
|
<span
|
||||||
|
className='text-reset pointer fw-bold'
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<Component />
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditInfo ({ item, canEdit, setCanEdit, onEdit, editText, editThreshold }) {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
if (canEdit) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span> \ </span>
|
||||||
|
<span
|
||||||
|
className='text-reset pointer fw-bold'
|
||||||
|
onClick={() => onEdit ? onEdit() : router.push(`/items/${item.id}/edit`)}
|
||||||
|
>
|
||||||
|
<span>{editText || 'edit'} </span>
|
||||||
|
{(!item.invoice?.actionState || item.invoice?.actionState === 'PAID') &&
|
||||||
|
<Countdown
|
||||||
|
date={editThreshold}
|
||||||
|
onComplete={() => { setCanEdit(false) }}
|
||||||
|
/>}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ function ItemLink ({ url, rel }) {
|
||||||
|
|
||||||
export default function Item ({
|
export default function Item ({
|
||||||
item, rank, belowTitle, right, full, children, itemClassName,
|
item, rank, belowTitle, right, full, children, itemClassName,
|
||||||
onQuoteReply, pinnable
|
onQuoteReply, pinnable, setDisableRetry, disableRetry
|
||||||
}) {
|
}) {
|
||||||
const titleRef = useRef()
|
const titleRef = useRef()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -139,6 +139,8 @@ export default function Item ({
|
||||||
onQuoteReply={onQuoteReply}
|
onQuoteReply={onQuoteReply}
|
||||||
pinnable={pinnable}
|
pinnable={pinnable}
|
||||||
extraBadges={Number(item?.user?.id) === USER_ID.ad && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
extraBadges={Number(item?.user?.id) === USER_ID.ad && <Badge className={styles.newComment} bg={null}>AD</Badge>}
|
||||||
|
setDisableRetry={setDisableRetry}
|
||||||
|
disableRetry={disableRetry}
|
||||||
/>
|
/>
|
||||||
{belowTitle}
|
{belowTitle}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { paidActionCacheMods } from './use-paid-mutation'
|
||||||
import { useRetryCreateItem } from './use-item-submit'
|
import { useRetryCreateItem } from './use-item-submit'
|
||||||
import { payBountyCacheMods } from './pay-bounty'
|
import { payBountyCacheMods } from './pay-bounty'
|
||||||
import { useToast } from './toast'
|
import { useToast } from './toast'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
|
||||||
function Notification ({ n, fresh }) {
|
function Notification ({ n, fresh }) {
|
||||||
const type = n.__typename
|
const type = n.__typename
|
||||||
|
@ -102,14 +103,14 @@ function NoteHeader ({ color, children, big }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoteItem ({ item }) {
|
function NoteItem ({ item, ...props }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{item.title
|
{item.title
|
||||||
? <Item item={item} itemClassName='pt-0' />
|
? <Item item={item} itemClassName='pt-0' {...props} />
|
||||||
: (
|
: (
|
||||||
<RootProvider root={item.root}>
|
<RootProvider root={item.root}>
|
||||||
<Comment item={item} noReply includeParent clickToContext />
|
<Comment item={item} noReply includeParent clickToContext {...props} />
|
||||||
</RootProvider>)}
|
</RootProvider>)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -343,7 +344,10 @@ function InvoicePaid ({ n }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function useActRetry ({ invoice }) {
|
function useActRetry ({ invoice }) {
|
||||||
const bountyCacheMods = invoice.item?.bounty ? payBountyCacheMods() : {}
|
const bountyCacheMods =
|
||||||
|
invoice.item.root?.bounty === invoice.satsRequested && invoice.item.root?.mine
|
||||||
|
? payBountyCacheMods()
|
||||||
|
: {}
|
||||||
return useAct({
|
return useAct({
|
||||||
query: RETRY_PAID_ACTION,
|
query: RETRY_PAID_ACTION,
|
||||||
onPayError: (e, cache, { data }) => {
|
onPayError: (e, cache, { data }) => {
|
||||||
|
@ -383,6 +387,7 @@ function Invoicification ({ n: { invoice, sortTime } }) {
|
||||||
const actRetry = useActRetry({ invoice })
|
const actRetry = useActRetry({ invoice })
|
||||||
const retryCreateItem = useRetryCreateItem({ id: invoice.item?.id })
|
const retryCreateItem = useRetryCreateItem({ id: invoice.item?.id })
|
||||||
const retryPollVote = usePollVote({ query: RETRY_PAID_ACTION, itemId: invoice.item?.id })
|
const retryPollVote = usePollVote({ query: RETRY_PAID_ACTION, itemId: invoice.item?.id })
|
||||||
|
const [disableRetry, setDisableRetry] = useState(false)
|
||||||
// XXX if we navigate to an invoice after it is retried in notifications
|
// XXX if we navigate to an invoice after it is retried in notifications
|
||||||
// the cache will clear invoice.item and will error on window.back
|
// the cache will clear invoice.item and will error on window.back
|
||||||
// alternatively, we could/should
|
// alternatively, we could/should
|
||||||
|
@ -407,7 +412,7 @@ function Invoicification ({ n: { invoice, sortTime } }) {
|
||||||
invoiceActionState = invoice.item.poll?.meInvoiceActionState
|
invoiceActionState = invoice.item.poll?.meInvoiceActionState
|
||||||
} else {
|
} else {
|
||||||
if (invoice.actionType === 'ZAP') {
|
if (invoice.actionType === 'ZAP') {
|
||||||
if (invoice.item.root?.bounty === invoice.satsRequested && invoice.item.root.mine) {
|
if (invoice.item.root?.bounty === invoice.satsRequested && invoice.item.root?.mine) {
|
||||||
actionString = 'bounty payment'
|
actionString = 'bounty payment'
|
||||||
} else {
|
} else {
|
||||||
actionString = 'zap'
|
actionString = 'zap'
|
||||||
|
@ -443,14 +448,19 @@ function Invoicification ({ n: { invoice, sortTime } }) {
|
||||||
<span className='ms-1 text-muted fw-light'> {numWithUnits(invoice.satsRequested)}</span>
|
<span className='ms-1 text-muted fw-light'> {numWithUnits(invoice.satsRequested)}</span>
|
||||||
<span className={invoiceActionState === 'FAILED' ? 'visible' : 'invisible'}>
|
<span className={invoiceActionState === 'FAILED' ? 'visible' : 'invisible'}>
|
||||||
<Button
|
<Button
|
||||||
size='sm' variant='outline-warning ms-2 border-1 rounded py-0'
|
size='sm' variant={classNames('outline-warning ms-2 border-1 rounded py-0', disableRetry && 'pulse')}
|
||||||
style={{ '--bs-btn-hover-color': '#fff', '--bs-btn-active-color': '#fff' }}
|
style={{ '--bs-btn-hover-color': '#fff', '--bs-btn-active-color': '#fff' }}
|
||||||
|
disabled={disableRetry}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
if (disableRetry) return
|
||||||
|
setDisableRetry(true)
|
||||||
try {
|
try {
|
||||||
const { error } = await retry({ variables: { invoiceId: parseInt(invoiceId) } })
|
const { error } = await retry({ variables: { invoiceId: parseInt(invoiceId) } })
|
||||||
if (error) throw error
|
if (error) throw error
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toaster.danger(error?.message || error?.toString?.())
|
toaster.danger(error?.message || error?.toString?.())
|
||||||
|
} finally {
|
||||||
|
setDisableRetry(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -459,7 +469,7 @@ function Invoicification ({ n: { invoice, sortTime } }) {
|
||||||
<span className='text-muted ms-2 fw-normal' suppressHydrationWarning>{timeSince(new Date(sortTime))}</span>
|
<span className='text-muted ms-2 fw-normal' suppressHydrationWarning>{timeSince(new Date(sortTime))}</span>
|
||||||
</span>
|
</span>
|
||||||
</NoteHeader>
|
</NoteHeader>
|
||||||
<NoteItem item={invoice.item} />
|
<NoteItem item={invoice.item} setDisableRetry={setDisableRetry} disableRetry={disableRetry} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,10 @@ export function usePaidMutation (mutation,
|
||||||
let { data, ...rest } = await mutate(innerOptions)
|
let { data, ...rest } = await mutate(innerOptions)
|
||||||
|
|
||||||
// use the most inner callbacks/options if they exist
|
// use the most inner callbacks/options if they exist
|
||||||
const { onPaid, onPayError, forceWaitForPayment, persistOnNavigate, update } = { ...options, ...innerOptions }
|
const {
|
||||||
|
onPaid, onPayError, forceWaitForPayment, persistOnNavigate,
|
||||||
|
update, waitFor = inv => inv?.actionState === 'PAID'
|
||||||
|
} = { ...options, ...innerOptions }
|
||||||
const ourOnCompleted = innerOnCompleted || onCompleted
|
const ourOnCompleted = innerOnCompleted || onCompleted
|
||||||
|
|
||||||
// get invoice without knowing the mutation name
|
// get invoice without knowing the mutation name
|
||||||
|
@ -95,7 +98,7 @@ export function usePaidMutation (mutation,
|
||||||
// the action is pessimistic
|
// the action is pessimistic
|
||||||
try {
|
try {
|
||||||
// wait for the invoice to be paid
|
// wait for the invoice to be paid
|
||||||
await waitForPayment(invoice, { alwaysShowQROnFailure: true, persistOnNavigate, waitFor: inv => inv?.actionState === 'PAID' })
|
await waitForPayment(invoice, { alwaysShowQROnFailure: true, persistOnNavigate, waitFor })
|
||||||
if (!response.result) {
|
if (!response.result) {
|
||||||
// if the mutation didn't return any data, ie pessimistic, we need to fetch it
|
// if the mutation didn't return any data, ie pessimistic, we need to fetch it
|
||||||
const { data: { paidAction } } = await getPaidAction({ variables: { invoiceId: parseInt(invoice.id) } })
|
const { data: { paidAction } } = await getPaidAction({ variables: { invoiceId: parseInt(invoice.id) } })
|
||||||
|
|
|
@ -263,6 +263,20 @@ $zindex-sticky: 900;
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pulse {
|
||||||
|
animation-name: pulse;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-duration: 0.66s;
|
||||||
|
animation-direction: alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 42%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
fill: var(--bs-body-color);
|
fill: var(--bs-body-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,10 +104,6 @@ async function work () {
|
||||||
await boss.work('paidActionCanceling', jobWrapper(paidActionCanceling))
|
await boss.work('paidActionCanceling', jobWrapper(paidActionCanceling))
|
||||||
await boss.work('paidActionFailed', jobWrapper(paidActionFailed))
|
await boss.work('paidActionFailed', jobWrapper(paidActionFailed))
|
||||||
await boss.work('paidActionPaid', jobWrapper(paidActionPaid))
|
await boss.work('paidActionPaid', jobWrapper(paidActionPaid))
|
||||||
// we renamed these jobs so we leave them so they can "migrate"
|
|
||||||
await boss.work('holdAction', jobWrapper(paidActionHeld))
|
|
||||||
await boss.work('settleActionError', jobWrapper(paidActionFailed))
|
|
||||||
await boss.work('settleAction', jobWrapper(paidActionPaid))
|
|
||||||
}
|
}
|
||||||
if (isServiceEnabled('search')) {
|
if (isServiceEnabled('search')) {
|
||||||
await boss.work('indexItem', jobWrapper(indexItem))
|
await boss.work('indexItem', jobWrapper(indexItem))
|
||||||
|
|
Loading…
Reference in New Issue