Merge pull request #965 from stackernews/zap-undo-threshold
Use thresholds to trigger zap undos
This commit is contained in:
commit
3a00695041
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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";
|
@ -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?
|
||||
|
Loading…
x
Reference in New Issue
Block a user