random zap amounts (#1263)

* add random zapping support

adds an option to enable random zap amounts per stacker

configurable in settings, you can enable this feature and provide
an upper and lower range of your random zap amount

* rename github eslint check to lint

this has been bothering me since we aren't using eslint for linting

* fixup! add random zapping support

* fixup! rename github eslint check to lint

* fixup! fixup! add random zapping support

---------

Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
SatsAllDay 2024-07-26 23:37:03 -04:00 committed by GitHub
parent 4964e2c7d1
commit dc0370ba17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 147 additions and 39 deletions

View File

@ -1,8 +1,8 @@
name: Eslint Check name: Lint Check
on: [pull_request] on: [pull_request]
jobs: jobs:
eslint-run: lint-run:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout

View File

@ -1004,6 +1004,12 @@ export default {
}) })
return relays?.map(r => r.nostrRelayAddr) return relays?.map(r => r.nostrRelayAddr)
},
tipRandom: async (user, args, { me }) => {
if (!me || me.id !== user.id) {
return false
}
return !!user.tipRandomMin && !!user.tipRandomMax
} }
}, },

View File

@ -98,6 +98,8 @@ export default gql`
noteItemMentions: Boolean! noteItemMentions: Boolean!
nsfwMode: Boolean! nsfwMode: Boolean!
tipDefault: Int! tipDefault: Int!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean! turboTipping: Boolean!
zapUndos: Int zapUndos: Int
wildWestMode: Boolean! wildWestMode: Boolean!
@ -165,6 +167,9 @@ export default gql`
noteItemMentions: Boolean! noteItemMentions: Boolean!
nsfwMode: Boolean! nsfwMode: Boolean!
tipDefault: Int! tipDefault: Int!
tipRandom: Boolean!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean! turboTipping: Boolean!
zapUndos: Int zapUndos: Int
wildWestMode: Boolean! wildWestMode: Boolean!

View File

@ -7,7 +7,7 @@ import UpBolt from '@/svgs/bolt.svg'
import { amountSchema } from '@/lib/validate' import { amountSchema } from '@/lib/validate'
import { useToast } from './toast' import { useToast } from './toast'
import { useLightning } from './lightning' import { useLightning } from './lightning'
import { nextTip } from './upvote' import { nextTip, defaultTipIncludingRandom } from './upvote'
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants' import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
import { usePaidMutation } from './use-paid-mutation' import { usePaidMutation } from './use-paid-mutation'
import { ACT_MUTATION } from '@/fragments/paidAction' import { ACT_MUTATION } from '@/fragments/paidAction'
@ -101,7 +101,7 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
return ( return (
<Form <Form
initial={{ initial={{
amount: me?.privates?.tipDefault || defaultTips[0], amount: defaultTipIncludingRandom(me?.privates) || defaultTips[0],
default: false default: false
}} }}
schema={amountSchema} schema={amountSchema}

View File

@ -26,7 +26,7 @@ const UpvotePopover = ({ target, show, handleClose }) => {
<button type='button' className='btn-close' onClick={handleClose}><span className='visually-hidden-focusable'>Close alert</span></button> <button type='button' className='btn-close' onClick={handleClose}><span className='visually-hidden-focusable'>Close alert</span></button>
</Popover.Header> </Popover.Header>
<Popover.Body> <Popover.Body>
<div className='mb-2'>Press the bolt again to zap {me?.privates?.tipDefault || 1} more sat{me?.privates?.tipDefault > 1 ? 's' : ''}.</div> <div className='mb-2'>Press the bolt again to zap {me?.privates?.tipRandom ? 'a random amount of' : `${me?.privates?.tipDefault || 1} more`} sat{me?.privates?.tipDefault > 1 ? 's' : ''}.</div>
<div>Repeatedly press the bolt to zap more sats.</div> <div>Repeatedly press the bolt to zap more sats.</div>
</Popover.Body> </Popover.Body>
</Popover> </Popover>
@ -67,11 +67,20 @@ export function DropdownItemUpVote ({ item }) {
) )
} }
export const nextTip = (meSats, { tipDefault, turboTipping }) => { export const defaultTipIncludingRandom = ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax }) =>
// what should our next tip be? tipRandom
if (!turboTipping) return (tipDefault || 1) ? Math.floor((Math.random() * (tipRandomMax - tipRandomMin)) + tipRandomMin)
: (tipDefault || 1)
let sats = tipDefault || 1 export const nextTip = (meSats, { tipDefault, turboTipping, tipRandom, tipRandomMin, tipRandomMax }) => {
// what should our next tip be?
const calculatedDefault = defaultTipIncludingRandom({ tipDefault, tipRandom, tipRandomMin, tipRandomMax })
if (!turboTipping) {
return calculatedDefault
}
let sats = calculatedDefault
if (turboTipping) { if (turboTipping) {
while (meSats >= sats) { while (meSats >= sats) {
sats *= 10 sats *= 10
@ -138,11 +147,17 @@ export default function UpVote ({ item, className }) {
// what should our next tip be? // what should our next tip be?
const sats = nextTip(meSats, { ...me?.privates }) const sats = nextTip(meSats, { ...me?.privates })
let overlayTextContent
if (me) {
overlayTextContent = me.privates?.tipRandom ? 'random' : numWithUnits(sats, { abbreviate: false })
} else {
overlayTextContent = 'zap it'
}
return [ return [
meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it', meSats, overlayTextContent,
getColor(meSats), getColor(meSats + sats)] getColor(meSats), getColor(meSats + sats)]
}, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault]) }, [item?.meSats, item?.meAnonSats, me?.privates?.tipDefault, me?.privates?.turboDefault, me?.privates?.tipRandom, me?.privates?.tipRandomMin, me?.privates?.tipRandomMax])
const handleModalClosed = () => { const handleModalClosed = () => {
setHover(false) setHover(false)

View File

@ -41,6 +41,9 @@ export const ME = gql`
noteItemMentions noteItemMentions
sats sats
tipDefault tipDefault
tipRandom
tipRandomMin
tipRandomMax
tipPopover tipPopover
turboTipping turboTipping
zapUndos zapUndos
@ -66,6 +69,9 @@ export const SETTINGS_FIELDS = gql`
fragment SettingsFields on User { fragment SettingsFields on User {
privates { privates {
tipDefault tipDefault
tipRandom
tipRandomMin
tipRandomMax
turboTipping turboTipping
zapUndos zapUndos
fiatCurrency fiatCurrency

View File

@ -540,8 +540,24 @@ export const actSchema = object({
act: string().required('required').oneOf(['TIP', 'DONT_LIKE_THIS']) act: string().required('required').oneOf(['TIP', 'DONT_LIKE_THIS'])
}) })
export const settingsSchema = object({ export const settingsSchema = object().shape({
tipDefault: intValidator.required('required').positive('must be positive'), tipDefault: intValidator.required('required').positive('must be positive'),
tipRandomMin: intValidator.nullable().positive('must be positive')
.when(['tipRandomMax'], ([max], schema) => {
let res = schema
if (max) {
res = schema.required('minimum and maximum must either both be omitted or specified').nonNullable()
}
return res.lessThan(max, 'must be less than maximum')
}),
tipRandomMax: intValidator.nullable().positive('must be positive')
.when(['tipRandomMin'], ([min], schema) => {
let res = schema
if (min) {
res = schema.required('minimum and maximum must either both be omitted or specified').nonNullable()
}
return res.moreThan(min, 'must be more than minimum')
}),
fiatCurrency: string().required('required').oneOf(SUPPORTED_CURRENCIES), fiatCurrency: string().required('required').oneOf(SUPPORTED_CURRENCIES),
withdrawMaxFeeDefault: intValidator.required('required').positive('must be positive'), withdrawMaxFeeDefault: intValidator.required('required').positive('must be positive'),
nostrPubkey: string().nullable() nostrPubkey: string().nullable()
@ -561,7 +577,8 @@ export const settingsSchema = object({
noReferralLinks: boolean(), noReferralLinks: boolean(),
hideIsContributor: boolean(), hideIsContributor: boolean(),
zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0') zapUndos: intValidator.nullable().min(0, 'must be greater or equal to 0')
}) // exclude from cyclic analysis. see https://github.com/jquense/yup/issues/720
}, [['tipRandomMax', 'tipRandomMin']])
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'
export const lastAuthRemovalSchema = object({ export const lastAuthRemovalSchema = object({

View File

@ -112,6 +112,9 @@ export default function Settings ({ ssrData }) {
<Form <Form
initial={{ initial={{
tipDefault: settings?.tipDefault || 21, tipDefault: settings?.tipDefault || 21,
tipRandom: settings?.tipRandom,
tipRandomMin: settings?.tipRandomMin || 1,
tipRandomMax: settings?.tipRandomMax || settings?.tipDefault || 21,
turboTipping: settings?.turboTipping, turboTipping: settings?.turboTipping,
zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100), zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100),
zapUndosEnabled: settings?.zapUndos !== null, zapUndosEnabled: settings?.zapUndos !== null,
@ -149,7 +152,7 @@ export default function Settings ({ ssrData }) {
noReferralLinks: settings?.noReferralLinks noReferralLinks: settings?.noReferralLinks
}} }}
schema={settingsSchema} schema={settingsSchema}
onSubmit={async ({ tipDefault, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => { onSubmit={async ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax, withdrawMaxFeeDefault, zapUndos, zapUndosEnabled, nostrPubkey, nostrRelays, ...values }) => {
if (nostrPubkey.length === 0) { if (nostrPubkey.length === 0) {
nostrPubkey = null nostrPubkey = null
} else { } else {
@ -166,6 +169,8 @@ export default function Settings ({ ssrData }) {
variables: { variables: {
settings: { settings: {
tipDefault: Number(tipDefault), tipDefault: Number(tipDefault),
tipRandomMin: tipRandom ? Number(tipRandomMin) : null,
tipRandomMax: tipRandom ? Number(tipRandomMax) : null,
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault), withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
zapUndos: zapUndosEnabled ? Number(zapUndos) : null, zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
nostrPubkey, nostrPubkey,
@ -190,7 +195,7 @@ export default function Settings ({ ssrData }) {
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
hint={<small className='text-muted'>note: you can also press and hold the lightning bolt to zap custom amounts</small>} hint={<small className='text-muted'>note: you can also press and hold the lightning bolt to zap custom amounts</small>}
/> />
<div className='mb-2'> <div className='pb-4'>
<AccordianItem <AccordianItem
show={settings?.turboTipping} show={settings?.turboTipping}
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>} header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>}
@ -224,6 +229,7 @@ export default function Settings ({ ssrData }) {
groupClassName='mb-0' groupClassName='mb-0'
/> />
<ZapUndosField /> <ZapUndosField />
<TipRandomField />
</> </>
} }
/> />
@ -995,31 +1001,79 @@ function ApiKeyDeleteObstacle ({ onClose }) {
const ZapUndosField = () => { const ZapUndosField = () => {
const [checkboxField] = useField({ name: 'zapUndosEnabled' }) const [checkboxField] = useField({ name: 'zapUndosEnabled' })
return ( return (
<div className='d-flex flex-row align-items-center'> <>
<Input <Checkbox
name='zapUndos' name='zapUndosEnabled'
disabled={!checkboxField.value} groupClassName='mb-0'
label={ label={
<Checkbox <div className='d-flex align-items-center'>
name='zapUndosEnabled' zap undos
groupClassName='mb-0' <Info>
label={ <ul className='fw-bold'>
<div className='d-flex align-items-center'> <li>After every zap that exceeds or is equal to the threshold, the bolt will pulse</li>
zap undos <li>You can undo the zap if you click the bolt while it's pulsing</li>
<Info> <li>The bolt will pulse for {ZAP_UNDO_DELAY_MS / 1000} seconds</li>
<ul className='fw-bold'> </ul>
<li>After every zap that exceeds or is equal to the threshold, the bolt will pulse</li> </Info>
<li>You can undo the zap if you click the bolt while it's pulsing</li> </div>
<li>The bolt will pulse for {ZAP_UNDO_DELAY_MS / 1000} seconds</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> {checkboxField.value &&
<Input
name='zapUndos'
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
hint={<small className='text-muted'>threshold at which undo button is shown</small>}
/>}
</>
)
}
const TipRandomField = () => {
const [tipRandomField] = useField({ name: 'tipRandom' })
const [tipRandomMinField] = useField({ name: 'tipRandomMin' })
const [tipRandomMaxField] = useField({ name: 'tipRandomMax' })
return (
<>
<Checkbox
name='tipRandom'
groupClassName='mb-0'
label={
<div className='d-flex align-items-center'>
Enable random zap values
<Info>
<ul className='fw-bold'>
<li>Set a minimum and maximum zap amount</li>
<li>Each time you zap something, a random amount of sats between your minimum and maximum will be zapped</li>
<li>If this setting is enabled, it will ignore your default zap amount</li>
</ul>
</Info>
</div>
}
/>
{tipRandomField.value &&
<>
<Input
type='number'
label='minimum random zap'
name='tipRandomMin'
disabled={!tipRandomField.value}
groupClassName='mb-1'
required
autoFocus
max={tipRandomMaxField.value ? tipRandomMaxField.value - 1 : undefined}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
<Input
type='number'
label='maximum random zap'
name='tipRandomMax'
disabled={!tipRandomField.value}
required
autoFocus
min={tipRandomMinField.value ? tipRandomMinField.value + 1 : undefined}
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
/>
</>}
</>
) )
} }

View File

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "users" ADD COLUMN "tipRandomMax" INTEGER,
ADD COLUMN "tipRandomMin" INTEGER;

View File

@ -30,6 +30,8 @@ model User {
apiKeyHash String? @unique(map: "users.apikeyhash_unique") @db.Char(64) apiKeyHash String? @unique(map: "users.apikeyhash_unique") @db.Char(64)
apiKeyEnabled Boolean @default(false) apiKeyEnabled Boolean @default(false)
tipDefault Int @default(100) tipDefault Int @default(100)
tipRandomMin Int?
tipRandomMax Int?
bioId Int? bioId Int?
inviteId String? inviteId String?
tipPopover Boolean @default(false) tipPopover Boolean @default(false)