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!
|
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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -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)
|
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?
|
||||||
|
Loading…
x
Reference in New Issue
Block a user