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! nsfwMode: Boolean!
tipDefault: Int! tipDefault: Int!
turboTipping: Boolean! turboTipping: Boolean!
zapUndos: Boolean! zapUndos: Int
wildWestMode: Boolean! wildWestMode: Boolean!
withdrawMaxFeeDefault: Int! withdrawMaxFeeDefault: Int!
} }
@ -157,7 +157,7 @@ export default gql`
nsfwMode: Boolean! nsfwMode: Boolean!
tipDefault: Int! tipDefault: Int!
turboTipping: Boolean! turboTipping: Boolean!
zapUndos: Boolean! zapUndos: Int
wildWestMode: Boolean! wildWestMode: Boolean!
withdrawMaxFeeDefault: Int! withdrawMaxFeeDefault: Int!
autoWithdrawThreshold: Int autoWithdrawThreshold: Int

View File

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

View File

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

View File

@ -530,7 +530,8 @@ export const settingsSchema = object({
hideWalletBalance: boolean(), hideWalletBalance: boolean(),
diagnostics: boolean(), diagnostics: boolean(),
noReferralLinks: 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' 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 { INVOICE_RETENTION_DAYS } from '@/lib/constants'
import { OverlayTrigger, Tooltip } from 'react-bootstrap' import { OverlayTrigger, Tooltip } from 'react-bootstrap'
import DeleteIcon from '@/svgs/delete-bin-line.svg' import DeleteIcon from '@/svgs/delete-bin-line.svg'
import { useField } from 'formik'
export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true }) export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true })
@ -65,7 +66,8 @@ export default function Settings ({ ssrData }) {
initial={{ initial={{
tipDefault: settings?.tipDefault || 21, tipDefault: settings?.tipDefault || 21,
turboTipping: settings?.turboTipping, turboTipping: settings?.turboTipping,
zapUndos: settings?.zapUndos, zapUndos: settings?.zapUndos || settings?.tipDefault ? 100 * settings.tipDefault : 2100,
zapUndosEnabled: settings?.zapUndos !== null,
fiatCurrency: settings?.fiatCurrency || 'USD', fiatCurrency: settings?.fiatCurrency || 'USD',
withdrawMaxFeeDefault: settings?.withdrawMaxFeeDefault, withdrawMaxFeeDefault: settings?.withdrawMaxFeeDefault,
noteItemSats: settings?.noteItemSats, noteItemSats: settings?.noteItemSats,
@ -98,7 +100,7 @@ export default function Settings ({ ssrData }) {
noReferralLinks: settings?.noReferralLinks noReferralLinks: settings?.noReferralLinks
}} }}
schema={settingsSchema} schema={settingsSchema}
onSubmit={async ({ tipDefault, withdrawMaxFeeDefault, nostrPubkey, nostrRelays, ...values }) => { onSubmit={async ({ tipDefault, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => {
if (nostrPubkey.length === 0) { if (nostrPubkey.length === 0) {
nostrPubkey = null nostrPubkey = null
} else { } else {
@ -116,6 +118,7 @@ export default function Settings ({ ssrData }) {
settings: { settings: {
tipDefault: Number(tipDefault), tipDefault: Number(tipDefault),
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault), withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
nostrPubkey, nostrPubkey,
nostrRelays: nostrRelaysFiltered, nostrRelays: nostrRelaysFiltered,
...values ...values
@ -171,25 +174,7 @@ export default function Settings ({ ssrData }) {
} }
groupClassName='mb-0' groupClassName='mb-0'
/> />
<Checkbox <ZapUndosField />
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>
}
/>
</> </>
} }
/> />
@ -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) autoDropBolt11s Boolean @default(false)
hideFromTopUsers Boolean @default(false) hideFromTopUsers Boolean @default(false)
turboTipping Boolean @default(false) turboTipping Boolean @default(false)
zapUndos Boolean @default(false) zapUndos Int?
imgproxyOnly Boolean @default(false) imgproxyOnly Boolean @default(false)
hideWalletBalance Boolean @default(false) hideWalletBalance Boolean @default(false)
referrerId Int? referrerId Int?