Merge pull request #965 from stackernews/zap-undo-threshold

Use thresholds to trigger zap undos
This commit is contained in:
Keyan 2024-03-25 17:34:10 -05:00 committed by GitHub
commit 3a00695041
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 70 additions and 36 deletions

View File

@ -92,7 +92,7 @@ export default gql`
nsfwMode: Boolean!
tipDefault: Int!
turboTipping: Boolean!
zapUndos: Boolean!
zapUndos: Int
wildWestMode: Boolean!
withdrawMaxFeeDefault: Int!
}
@ -157,7 +157,7 @@ export default gql`
nsfwMode: Boolean!
tipDefault: Int!
turboTipping: Boolean!
zapUndos: Boolean!
zapUndos: Int
wildWestMode: Boolean!
withdrawMaxFeeDefault: Int!
autoWithdrawThreshold: Int

View File

@ -1,7 +1,7 @@
import Dropdown from 'react-bootstrap/Dropdown'
import { useShowModal } from './modal'
import { useToast } from './toast'
import ItemAct from './item-act'
import ItemAct, { zapUndosThresholdReached } from './item-act'
import AccordianItem from './accordian-item'
import Flag from '@/svgs/flag-fill.svg'
import { useMemo } from 'react'
@ -32,12 +32,11 @@ function DownZapper ({ id, As, children }) {
try {
showModal(onClose =>
<ItemAct
onClose={() => {
onClose={(amount) => {
onClose()
// undo prompt was toasted before closing modal if zap undos are enabled
// so an additional success toast would be confusing
const zapUndosEnabled = me && me?.privates?.zapUndos
if (!zapUndosEnabled) toaster.success('item downzapped')
if (!zapUndosThresholdReached(me, amount)) toaster.success('item downzapped')
}} itemId={id} down
>
<AccordianItem

View File

@ -41,6 +41,12 @@ const addCustomTip = (amount) => {
window.localStorage.setItem('custom-tips', JSON.stringify(customTips))
}
export const zapUndosThresholdReached = (me, amount) => {
if (!me) return false
const enabled = me.privates.zapUndos !== null
return enabled ? amount >= me.privates.zapUndos : false
}
export default function ItemAct ({ onClose, itemId, down, children }) {
const inputRef = useRef(null)
const me = useMe()
@ -73,9 +79,9 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
})
// only strike when zap undos not enabled
// due to optimistic UX on zap undos
if (!me || !me.privates.zapUndos) await strike()
if (!zapUndosThresholdReached(me, Number(amount))) await strike()
addCustomTip(Number(amount))
if (!keepOpen) onClose()
if (!keepOpen) onClose(Number(amount))
}, [me, act, down, itemId, strike])
const onSubmitWithUndos = withToastFlow(toaster)(
@ -123,7 +129,7 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
return onSubmit(values, { flowId, ...args, update: null })
}
await strike()
onClose()
onClose(sats)
return new Promise((resolve, reject) => {
undoUpdate = update()
setTimeout(() => {
@ -156,7 +162,12 @@ export default function ItemAct ({ onClose, itemId, down, children }) {
}}
schema={amountSchema}
invoiceable
onSubmit={me?.privates?.zapUndos ? onSubmitWithUndos : onSubmit}
onSubmit={(values, ...args) => {
if (zapUndosThresholdReached(me, values.amount)) {
return onSubmitWithUndos(values, ...args)
}
return onSubmit(values, ...args)
}}
>
<Input
label='amount'
@ -376,16 +387,17 @@ export function useZap () {
// add current sats to next tip since idempotent zaps use desired total zap not difference
const sats = meSats + nextTip(meSats, { ...me?.privates })
const amount = sats - meSats
const variables = { id: item.id, sats, act: 'TIP', amount: sats - meSats }
const insufficientFunds = me?.privates.sats < (sats - meSats)
const variables = { id: item.id, sats, act: 'TIP', amount }
const insufficientFunds = me?.privates.sats < amount
const optimisticResponse = { act: { path: item.path, ...variables } }
const flowId = (+new Date()).toString(16)
const zapArgs = { variables, optimisticResponse: insufficientFunds ? null : optimisticResponse, update, flowId }
try {
if (insufficientFunds) throw new Error('insufficient funds')
strike()
if (me?.privates?.zapUndos) {
if (zapUndosThresholdReached(me, amount)) {
await zapWithUndos(zapArgs)
} else {
await zap(zapArgs)

View File

@ -530,7 +530,8 @@ export const settingsSchema = object({
hideWalletBalance: boolean(),
diagnostics: boolean(),
noReferralLinks: boolean(),
hideIsContributor: boolean()
hideIsContributor: boolean(),
zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0')
})
const warningMessage = 'If I logout, even accidentally, I will never be able to access my account again'

View File

@ -28,6 +28,7 @@ import { useMe } from '@/components/me'
import { INVOICE_RETENTION_DAYS } from '@/lib/constants'
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import DeleteIcon from '@/svgs/delete-bin-line.svg'
import { useField } from 'formik'
export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true })
@ -65,7 +66,8 @@ export default function Settings ({ ssrData }) {
initial={{
tipDefault: settings?.tipDefault || 21,
turboTipping: settings?.turboTipping,
zapUndos: settings?.zapUndos,
zapUndos: settings?.zapUndos || settings?.tipDefault ? 100 * settings.tipDefault : 2100,
zapUndosEnabled: settings?.zapUndos !== null,
fiatCurrency: settings?.fiatCurrency || 'USD',
withdrawMaxFeeDefault: settings?.withdrawMaxFeeDefault,
noteItemSats: settings?.noteItemSats,
@ -98,7 +100,7 @@ export default function Settings ({ ssrData }) {
noReferralLinks: settings?.noReferralLinks
}}
schema={settingsSchema}
onSubmit={async ({ tipDefault, withdrawMaxFeeDefault, nostrPubkey, nostrRelays, ...values }) => {
onSubmit={async ({ tipDefault, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => {
if (nostrPubkey.length === 0) {
nostrPubkey = null
} else {
@ -116,6 +118,7 @@ export default function Settings ({ ssrData }) {
settings: {
tipDefault: Number(tipDefault),
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
nostrPubkey,
nostrRelays: nostrRelaysFiltered,
...values
@ -171,25 +174,7 @@ export default function Settings ({ ssrData }) {
}
groupClassName='mb-0'
/>
<Checkbox
name='zapUndos'
label={
<div className='d-flex align-items-center'>zap undos
<Info>
<ul className='fw-bold'>
<li>An undo button is shown after every zap</li>
<li>The button is shown for 5 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>
</Info>
</div>
}
/>
<ZapUndosField />
</>
}
/>
@ -920,3 +905,36 @@ I estimate that I will call the GraphQL API this many times (rough estimate is f
</>
)
}
const ZapUndosField = () => {
const [checkboxField] = useField({ name: 'zapUndosEnabled' })
return (
<div className='d-flex flex-row align-items-center'>
<Input
name='zapUndos'
disabled={!checkboxField.value}
label={
<Checkbox
name='zapUndosEnabled'
groupClassName='mb-0'
label={
<div className='d-flex align-items-center'>
zap undos
<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 only shown for zaps from the custodial wallet</li>
<li>Use a budget or manual approval with attached wallets</li>
</ul>
</Info>
</div>
}
/>
}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
hint={<small className='text-muted'>threshold at which undo button is shown</small>}
/>
</div>
)
}

View File

@ -0,0 +1,4 @@
ALTER TABLE "users" ADD COLUMN "zapUndosTmp" INTEGER;
UPDATE "users" SET "zapUndosTmp" = CASE WHEN "zapUndos" = false THEN NULL ELSE 0::INTEGER END;
ALTER TABLE "users" DROP COLUMN "zapUndos";
ALTER TABLE "users" RENAME COLUMN "zapUndosTmp" TO "zapUndos";

View File

@ -56,7 +56,7 @@ model User {
autoDropBolt11s Boolean @default(false)
hideFromTopUsers Boolean @default(false)
turboTipping Boolean @default(false)
zapUndos Boolean @default(false)
zapUndos Int?
imgproxyOnly Boolean @default(false)
hideWalletBalance Boolean @default(false)
referrerId Int?