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