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]
jobs:
eslint-run:
lint-run:
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

@ -1004,6 +1004,12 @@ export default {
})
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!
nsfwMode: Boolean!
tipDefault: Int!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean!
zapUndos: Int
wildWestMode: Boolean!
@ -165,6 +167,9 @@ export default gql`
noteItemMentions: Boolean!
nsfwMode: Boolean!
tipDefault: Int!
tipRandom: Boolean!
tipRandomMin: Int
tipRandomMax: Int
turboTipping: Boolean!
zapUndos: Int
wildWestMode: Boolean!

View File

@ -7,7 +7,7 @@ import UpBolt from '@/svgs/bolt.svg'
import { amountSchema } from '@/lib/validate'
import { useToast } from './toast'
import { useLightning } from './lightning'
import { nextTip } from './upvote'
import { nextTip, defaultTipIncludingRandom } from './upvote'
import { ZAP_UNDO_DELAY_MS } from '@/lib/constants'
import { usePaidMutation } from './use-paid-mutation'
import { ACT_MUTATION } from '@/fragments/paidAction'
@ -101,7 +101,7 @@ export default function ItemAct ({ onClose, item, down, children, abortSignal })
return (
<Form
initial={{
amount: me?.privates?.tipDefault || defaultTips[0],
amount: defaultTipIncludingRandom(me?.privates) || defaultTips[0],
default: false
}}
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>
</Popover.Header>
<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>
</Popover.Body>
</Popover>
@ -67,11 +67,20 @@ export function DropdownItemUpVote ({ item }) {
)
}
export const nextTip = (meSats, { tipDefault, turboTipping }) => {
// what should our next tip be?
if (!turboTipping) return (tipDefault || 1)
export const defaultTipIncludingRandom = ({ tipDefault, tipRandom, tipRandomMin, tipRandomMax }) =>
tipRandom
? 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) {
while (meSats >= sats) {
sats *= 10
@ -138,11 +147,17 @@ export default function UpVote ({ item, className }) {
// what should our next tip be?
const sats = nextTip(meSats, { ...me?.privates })
let overlayTextContent
if (me) {
overlayTextContent = me.privates?.tipRandom ? 'random' : numWithUnits(sats, { abbreviate: false })
} else {
overlayTextContent = 'zap it'
}
return [
meSats, me ? numWithUnits(sats, { abbreviate: false }) : 'zap it',
meSats, overlayTextContent,
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 = () => {
setHover(false)

View File

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

View File

@ -540,8 +540,24 @@ export const actSchema = object({
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'),
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),
withdrawMaxFeeDefault: intValidator.required('required').positive('must be positive'),
nostrPubkey: string().nullable()
@ -561,7 +577,8 @@ export const settingsSchema = object({
noReferralLinks: boolean(),
hideIsContributor: boolean(),
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'
export const lastAuthRemovalSchema = object({

View File

@ -112,6 +112,9 @@ export default function Settings ({ ssrData }) {
<Form
initial={{
tipDefault: settings?.tipDefault || 21,
tipRandom: settings?.tipRandom,
tipRandomMin: settings?.tipRandomMin || 1,
tipRandomMax: settings?.tipRandomMax || settings?.tipDefault || 21,
turboTipping: settings?.turboTipping,
zapUndos: settings?.zapUndos || (settings?.tipDefault ? 100 * settings.tipDefault : 2100),
zapUndosEnabled: settings?.zapUndos !== null,
@ -149,7 +152,7 @@ export default function Settings ({ ssrData }) {
noReferralLinks: settings?.noReferralLinks
}}
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) {
nostrPubkey = null
} else {
@ -166,6 +169,8 @@ export default function Settings ({ ssrData }) {
variables: {
settings: {
tipDefault: Number(tipDefault),
tipRandomMin: tipRandom ? Number(tipRandomMin) : null,
tipRandomMax: tipRandom ? Number(tipRandomMax) : null,
withdrawMaxFeeDefault: Number(withdrawMaxFeeDefault),
zapUndos: zapUndosEnabled ? Number(zapUndos) : null,
nostrPubkey,
@ -190,7 +195,7 @@ export default function Settings ({ ssrData }) {
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>}
/>
<div className='mb-2'>
<div className='pb-4'>
<AccordianItem
show={settings?.turboTipping}
header={<div style={{ fontWeight: 'bold', fontSize: '92%' }}>advanced</div>}
@ -224,6 +229,7 @@ export default function Settings ({ ssrData }) {
groupClassName='mb-0'
/>
<ZapUndosField />
<TipRandomField />
</>
}
/>
@ -995,31 +1001,79 @@ function ApiKeyDeleteObstacle ({ onClose }) {
const ZapUndosField = () => {
const [checkboxField] = useField({ name: 'zapUndosEnabled' })
return (
<div className='d-flex flex-row align-items-center'>
<Input
name='zapUndos'
disabled={!checkboxField.value}
<>
<Checkbox
name='zapUndosEnabled'
groupClassName='mb-0'
label={
<Checkbox
name='zapUndosEnabled'
groupClassName='mb-0'
label={
<div className='d-flex align-items-center'>
zap undos
<Info>
<ul className='fw-bold'>
<li>After every zap that exceeds or is equal to the threshold, the bolt will pulse</li>
<li>You can undo the zap if you click the bolt while it's pulsing</li>
<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 className='d-flex align-items-center'>
zap undos
<Info>
<ul className='fw-bold'>
<li>After every zap that exceeds or is equal to the threshold, the bolt will pulse</li>
<li>You can undo the zap if you click the bolt while it's pulsing</li>
<li>The bolt will pulse for {ZAP_UNDO_DELAY_MS / 1000} seconds</li>
</ul>
</Info>
</div>
}
/>
</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)
apiKeyEnabled Boolean @default(false)
tipDefault Int @default(100)
tipRandomMin Int?
tipRandomMax Int?
bioId Int?
inviteId String?
tipPopover Boolean @default(false)