Skip to content

Commit

Permalink
Use AbortController for zap undos
Browse files Browse the repository at this point in the history
  • Loading branch information
ekzyis committed May 23, 2024
1 parent 6741fab commit 711ed36
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 14 deletions.
9 changes: 6 additions & 3 deletions components/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { InvoiceCanceledError, usePayment } from './payment'
import { useMe } from './me'
import { optimisticUpdate } from '@/lib/apollo'
import { useClientNotifications } from './client-notifications'
import { ActCanceledError } from './item-act'

export class SessionRequiredError extends Error {
constructor () {
Expand Down Expand Up @@ -804,7 +805,7 @@ const StorageKeyPrefixContext = createContext()
export function Form ({
initial, schema, onSubmit, children, initialError, validateImmediately,
storageKeyPrefix, validateOnChange = true, prepaid, postpaid, requireSession, innerRef,
optimisticUpdate: optimisticUpdateArgs, clientNotification, ...props
optimisticUpdate: optimisticUpdateArgs, clientNotification, signal, ...props
}) {
const toaster = useToast()
const initialErrorToasted = useRef(false)
Expand Down Expand Up @@ -848,6 +849,8 @@ export function Form ({
revert = optimisticUpdate(optimisticUpdateArgs(variables))
}

await signal?.pause({ me, amount })

if (me && clientNotification) {
nid = notify(clientNotification.PENDING, variables)
}
Expand All @@ -871,7 +874,7 @@ export function Form ({
} catch (err) {
revert?.()

if (err instanceof InvoiceCanceledError) {
if (err instanceof InvoiceCanceledError || err instanceof ActCanceledError) {
return
}

Expand All @@ -889,7 +892,7 @@ export function Form ({
// stored in localStorage to handle this case.
if (nid) unnotify(nid)
}
}, [me, onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix, payment])
}, [me, onSubmit, feeButton?.total, toaster, clearLocalStorage, storageKeyPrefix, payment, signal])

return (
<Formik
Expand Down
53 changes: 50 additions & 3 deletions components/item-act.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { nextTip } from './upvote'
import { InvoiceCanceledError, usePayment } from './payment'
import { optimisticUpdate } from '@/lib/apollo'
import { Types as ClientNotification, ClientNotifyProvider, useClientNotifications } from './client-notifications'
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'

const defaultTips = [100, 1000, 10_000, 100_000]

Expand Down Expand Up @@ -101,7 +102,7 @@ export const actUpdate = ({ me, onUpdate }) => (cache, args) => {
onUpdate?.(cache, args)
}

export default function ItemAct ({ onClose, item, down, children }) {
export default function ItemAct ({ onClose, item, down, children, abortSignal }) {
const inputRef = useRef(null)
const me = useMe()
const [oValue, setOValue] = useState()
Expand Down Expand Up @@ -151,6 +152,7 @@ export default function ItemAct ({ onClose, item, down, children }) {
optimisticUpdate={optimisticUpdate}
onSubmit={onSubmit}
clientNotification={ClientNotification.Zap}
signal={abortSignal}
>
<Input
label='amount'
Expand Down Expand Up @@ -253,7 +255,7 @@ export function useZap () {
const strike = useLightning()
const payment = usePayment()

return useCallback(async ({ item, me }) => {
return useCallback(async ({ item, mem, abortSignal }) => {
const meSats = (item?.meSats || 0)

// add current sats to next tip since idempotent zaps use desired total zap not difference
Expand All @@ -269,6 +271,8 @@ export function useZap () {
revert = optimisticUpdate({ mutation: ZAP_MUTATION, variables, optimisticResponse, update })
strike()

await abortSignal.pause({ me, amount: satsDelta })

if (me) {
nid = notify(ClientNotification.Zap.PENDING, notifyProps)
}
Expand All @@ -279,7 +283,7 @@ export function useZap () {
} catch (error) {
revert?.()

if (error instanceof InvoiceCanceledError) {
if (error instanceof InvoiceCanceledError || error instanceof ActCanceledError) {
return
}

Expand All @@ -296,3 +300,46 @@ export function useZap () {
}
}, [me?.id, strike, payment, notify, unnotify])
}

export class ActCanceledError extends Error {
constructor () {
super('act canceled')
this.name = 'ActCanceledError'
}
}

export class ZapUndoController extends AbortController {
constructor () {
super()
this.signal.start = () => { this.started = true }
this.signal.done = () => { this.done = true }
this.signal.pause = async ({ me, amount }) => {
if (zapUndoTrigger({ me, amount })) {
await zapUndo(this.signal)
}
}
}
}

const zapUndoTrigger = ({ me, amount }) => {
if (!me) return false
const enabled = me.privates.zapUndos !== null
return enabled ? amount >= me.privates.zapUndos : false
}

const zapUndo = async (signal) => {
return await new Promise((resolve, reject) => {
signal.start()
const abortHandler = () => {
reject(new ActCanceledError())
signal.done()
signal.removeEventListener('abort', abortHandler)
}
signal.addEventListener('abort', abortHandler)
setTimeout(() => {
resolve()
signal.done()
signal.removeEventListener('abort', abortHandler)
}, ZAP_UNDO_DELAY_MS)
})
}
28 changes: 24 additions & 4 deletions components/upvote.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, { useZap } from './item-act'
import ItemAct, { ZapUndoController, useZap } from './item-act'
import { useMe } from './me'
import getColor from '@/lib/rainbow'
import { useCallback, useMemo, useRef, useState } from 'react'
Expand Down Expand Up @@ -97,6 +97,9 @@ export default function UpVote ({ item, className }) {
}`
)

const [controller, setController] = useState(null)
const pending = controller?.started && !controller.done

const setVoteShow = useCallback((yes) => {
if (!me) return

Expand Down Expand Up @@ -154,8 +157,16 @@ export default function UpVote ({ item, className }) {
}

setTipShow(false)

if (pending) {
controller.abort()
return
}
const c = new ZapUndoController()
setController(c)

showModal(onClose =>
<ItemAct onClose={onClose} item={item} />, { onClose: handleModalClosed })
<ItemAct onClose={onClose} item={item} abortSignal={c.signal} />, { onClose: handleModalClosed })
}

const handleShortPress = async () => {
Expand All @@ -172,7 +183,15 @@ export default function UpVote ({ item, className }) {
} else {
setTipShow(true)
}
zap({ item, me })

if (pending) {
controller.abort()
return
}
const c = new ZapUndoController()
setController(c)

await zap({ item, me, abortSignal: c.signal })
} else {
showModal(onClose => <ItemAct onClose={onClose} item={item} />, { onClose: handleModalClosed })
}
Expand Down Expand Up @@ -200,7 +219,8 @@ export default function UpVote ({ item, className }) {
`${styles.upvote}
${className || ''}
${disabled ? styles.noSelfTips : ''}
${meSats ? styles.voted : ''}`
${meSats ? styles.voted : ''}
${pending ? styles.pending : ''}`
}
style={meSats || hover
? {
Expand Down
16 changes: 15 additions & 1 deletion components/upvote.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,18 @@
position: absolute;
left: 4px;
width: 17px;
}
}

.pending {
animation-name: pulse;
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-duration: 0.25s;
animation-direction: alternate;
}

@keyframes pulse {
0% {
fill: #a5a5a5;
}
}
2 changes: 1 addition & 1 deletion lib/apollo.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ function getClient (uri) {
export function optimisticUpdate ({ mutation, variables, optimisticResponse, update }) {
const { cache, queryManager } = getApolloClient()

const mutationId = String(queryManager.mutationIdCounter)
const mutationId = String(queryManager.mutationIdCounter++)
queryManager.markMutationOptimistic(optimisticResponse, {
mutationId,
document: mutation,
Expand Down
2 changes: 2 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,5 @@ export const getWalletBy = (key, value) => {
}
throw new Error(`wallet not found: ${key}=${value}`)
}

export const ZAP_UNDO_DELAY_MS = 5_000
4 changes: 2 additions & 2 deletions pages/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { NostrAuth } from '@/components/nostr-auth'
import { useToast } from '@/components/toast'
import { useServiceWorkerLogger } from '@/components/logger'
import { useMe } from '@/components/me'
import { INVOICE_RETENTION_DAYS } from '@/lib/constants'
import { INVOICE_RETENTION_DAYS, ZAP_UNDO_DELAY_MS } from '@/lib/constants'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import DeleteIcon from '@/svgs/delete-bin-line.svg'
import { useField } from 'formik'
Expand Down Expand Up @@ -1007,7 +1007,7 @@ const ZapUndosField = () => {
<Info>
<ul className='fw-bold'>
<li>An undo button is shown after every zap that exceeds or is equal to the threshold</li>
<li>The button is shown for 5 seconds</li>
<li>The button is shown for {ZAP_UNDO_DELAY_MS / 1000} seconds</li>
<li>The button is only shown for zaps from the custodial wallet</li>
<li>Use a budget or manual approval with attached wallets</li>
</ul>
Expand Down

0 comments on commit 711ed36

Please sign in to comment.