Refactor animations (#2261)

* Fix fireworks not checking localStorage flag

* Refactor animations

* Don't import unused animations

* Remove unused hook

---------

Co-authored-by: k00b <k00b@stacker.news>
This commit is contained in:
ekzyis 2025-07-07 21:34:37 +02:00 committed by GitHub
parent 3a27057781
commit 18a38d8363
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 111 additions and 79 deletions

View File

@ -7,7 +7,7 @@ import {
setRangeValue, setRangeValue,
stringToRgb stringToRgb
} from 'tsparticles-engine' } from 'tsparticles-engine'
import useDarkMode from './dark-mode' import useDarkMode from '@/components/dark-mode'
export const FireworksContext = createContext({ export const FireworksContext = createContext({
strike: () => {} strike: () => {}

View File

@ -0,0 +1,72 @@
import { useCallback, useEffect, useState } from 'react'
import { useMe } from '@/components/me'
import { randInRange } from '@/lib/rand'
// import { LightningProvider, useLightning } from './lightning'
import { FireworksProvider, useFireworks } from './fireworks'
// import { SnowProvider, useSnow } from './snow'
const [SelectedAnimationProvider, useSelectedAnimation] = [
// LightningProvider, useLightning
FireworksProvider, useFireworks
// SnowProvider, useSnow // TODO: the snow animation doesn't seem to work anymore
]
export function AnimationProvider ({ children }) {
return (
<SelectedAnimationProvider>
<AnimationHooks>
{children}
</AnimationHooks>
</SelectedAnimationProvider>
)
}
export function useAnimation () {
const animate = useSelectedAnimation()
return useCallback(() => {
const should = window.localStorage.getItem('lnAnimate') || 'yes'
if (should !== 'yes') return false
animate()
return true
}, [animate])
}
export function useAnimationEnabled () {
const [enabled, setEnabled] = useState(undefined)
useEffect(() => {
const enabled = window.localStorage.getItem('lnAnimate') || 'yes'
setEnabled(enabled === 'yes')
}, [])
const toggleEnabled = useCallback(() => {
setEnabled(enabled => {
const newEnabled = !enabled
window.localStorage.setItem('lnAnimate', newEnabled ? 'yes' : 'no')
return newEnabled
})
}, [])
return [enabled, toggleEnabled]
}
function AnimationHooks ({ children }) {
const { me } = useMe()
const animate = useAnimation()
useEffect(() => {
if (me || window.localStorage.getItem('striked') || window.localStorage.getItem('lnAnimated')) return
const timeout = setTimeout(() => {
const animated = animate()
if (animated) {
window.localStorage.setItem('lnAnimated', 'yep')
}
}, randInRange(3000, 10000))
return () => clearTimeout(timeout)
}, [me?.id, animate])
return children
}

View File

@ -13,16 +13,11 @@ export class LightningProvider extends React.Component {
* @returns boolean indicating whether the strike actually happened, based on user preferences * @returns boolean indicating whether the strike actually happened, based on user preferences
*/ */
strike = () => { strike = () => {
const should = window.localStorage.getItem('lnAnimate') || 'yes' this.setState(state => {
if (should === 'yes') { return {
this.setState(state => { bolts: [...state.bolts, <Lightning key={state.bolts.length} onDone={() => this.unstrike(state.bolts.length)} />]
return { }
bolts: [...state.bolts, <Lightning key={state.bolts.length} onDone={() => this.unstrike(state.bolts.length)} />] })
}
})
return true
}
return false
} }
unstrike = (index) => { unstrike = (index) => {

View File

@ -11,21 +11,16 @@ export const SnowProvider = ({ children }) => {
const [flakes, setFlakes] = useState(Array(1024)) const [flakes, setFlakes] = useState(Array(1024))
const snow = useCallback(() => { const snow = useCallback(() => {
const should = window.localStorage.getItem('lnAnimate') || 'yes' // amount of flakes to add
if (should === 'yes') { const n = Math.floor(randInRange(5, 30))
// amount of flakes to add const newFlakes = [...flakes]
const n = Math.floor(randInRange(5, 30)) let i
const newFlakes = [...flakes] for (i = startIndex; i < (startIndex + n); ++i) {
let i const key = startIndex + i
for (i = startIndex; i < (startIndex + n); ++i) { newFlakes[i % MAX_FLAKES] = <Snow key={key} />
const key = startIndex + i
newFlakes[i % MAX_FLAKES] = <Snow key={key} />
}
setStartIndex(i % MAX_FLAKES)
setFlakes(newFlakes)
return true
} }
return false setStartIndex(i % MAX_FLAKES)
setFlakes(newFlakes)
}, [setFlakes, startIndex]) }, [setFlakes, startIndex])
return ( return (

View File

@ -12,10 +12,10 @@ import No from '@/svgs/no.svg'
import Bolt from '@/svgs/bolt.svg' import Bolt from '@/svgs/bolt.svg'
import Amboss from '@/svgs/amboss.svg' import Amboss from '@/svgs/amboss.svg'
import Mempool from '@/svgs/bimi.svg' import Mempool from '@/svgs/bimi.svg'
import { useEffect, useState } from 'react'
import Rewards from './footer-rewards' import Rewards from './footer-rewards'
import useDarkMode from './dark-mode' import useDarkMode from './dark-mode'
import ActionTooltip from './action-tooltip' import ActionTooltip from './action-tooltip'
import { useAnimationEnabled } from '@/components/animation'
const RssPopover = ( const RssPopover = (
<Popover> <Popover>
@ -145,24 +145,10 @@ const LegalPopover = (
export default function Footer ({ links = true }) { export default function Footer ({ links = true }) {
const [darkMode, darkModeToggle] = useDarkMode() const [darkMode, darkModeToggle] = useDarkMode()
const [lightning, setLightning] = useState(undefined) const [animationEnabled, toggleAnimation] = useAnimationEnabled()
useEffect(() => {
setLightning(window.localStorage.getItem('lnAnimate') || 'yes')
}, [])
const toggleLightning = () => {
if (lightning === 'yes') {
window.localStorage.setItem('lnAnimate', 'no')
setLightning('no')
} else {
window.localStorage.setItem('lnAnimate', 'yes')
setLightning('yes')
}
}
const DarkModeIcon = darkMode ? Sun : Moon const DarkModeIcon = darkMode ? Sun : Moon
const LnIcon = lightning === 'yes' ? No : Bolt const LnIcon = animationEnabled ? No : Bolt
const version = process.env.NEXT_PUBLIC_COMMIT_HASH const version = process.env.NEXT_PUBLIC_COMMIT_HASH
@ -175,8 +161,8 @@ export default function Footer ({ links = true }) {
<ActionTooltip notForm overlayText={`${darkMode ? 'disable' : 'enable'} dark mode`}> <ActionTooltip notForm overlayText={`${darkMode ? 'disable' : 'enable'} dark mode`}>
<DarkModeIcon onClick={darkModeToggle} width={20} height={20} className='fill-grey theme' suppressHydrationWarning /> <DarkModeIcon onClick={darkModeToggle} width={20} height={20} className='fill-grey theme' suppressHydrationWarning />
</ActionTooltip> </ActionTooltip>
<ActionTooltip notForm overlayText={`${lightning === 'yes' ? 'disable' : 'enable'} lightning animations`}> <ActionTooltip notForm overlayText={`${animationEnabled ? 'disable' : 'enable'} lightning animations`}>
<LnIcon onClick={toggleLightning} width={20} height={20} className='ms-2 fill-grey theme' suppressHydrationWarning /> <LnIcon onClick={toggleAnimation} width={20} height={20} className='ms-2 fill-grey theme' suppressHydrationWarning />
</ActionTooltip> </ActionTooltip>
</div> </div>
<div className='mb-0' style={{ fontWeight: 500 }}> <div className='mb-0' style={{ fontWeight: 500 }}>

View File

@ -13,7 +13,7 @@ import { ACT_MUTATION } from '@/fragments/paidAction'
import { meAnonSats } from '@/lib/apollo' import { meAnonSats } from '@/lib/apollo'
import { BoostItemInput } from './adv-post-form' import { BoostItemInput } from './adv-post-form'
import { useSendWallets } from '@/wallets/index' import { useSendWallets } from '@/wallets/index'
import { useFireworks } from './fireworks' import { useAnimation } from '@/components/animation'
const defaultTips = [100, 1000, 10_000, 100_000] const defaultTips = [100, 1000, 10_000, 100_000]
@ -96,7 +96,7 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
}, [onClose, item.id]) }, [onClose, item.id])
const actor = useAct() const actor = useAct()
const strike = useFireworks() const animate = useAnimation()
const onSubmit = useCallback(async ({ amount }) => { const onSubmit = useCallback(async ({ amount }) => {
if (abortSignal && zapUndoTrigger({ me, amount })) { if (abortSignal && zapUndoTrigger({ me, amount })) {
@ -111,7 +111,7 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
} }
const onPaid = () => { const onPaid = () => {
strike() animate()
onClose?.() onClose?.()
if (!me) setItemMeAnonSats({ id: item.id, amount }) if (!me) setItemMeAnonSats({ id: item.id, amount })
} }
@ -143,7 +143,7 @@ export default function ItemAct ({ onClose, item, act = 'TIP', step, children, a
}) })
if (error) throw error if (error) throw error
addCustomTip(Number(amount)) addCustomTip(Number(amount))
}, [me, actor, wallets.length, act, item.id, onClose, abortSignal, strike]) }, [me, actor, wallets.length, act, item.id, onClose, abortSignal, animate])
return act === 'BOOST' return act === 'BOOST'
? <BoostForm step={step} onSubmit={onSubmit} item={item} inputRef={inputRef} act={act}>{children}</BoostForm> ? <BoostForm step={step} onSubmit={onSubmit} item={item} inputRef={inputRef} act={act}>{children}</BoostForm>
@ -300,7 +300,7 @@ export function useAct ({ query = ACT_MUTATION, ...options } = {}) {
export function useZap () { export function useZap () {
const wallets = useSendWallets() const wallets = useSendWallets()
const act = useAct() const act = useAct()
const strike = useFireworks() const animate = useAnimation()
const toaster = useToast() const toaster = useToast()
return useCallback(async ({ item, me, abortSignal }) => { return useCallback(async ({ item, me, abortSignal }) => {
@ -314,7 +314,7 @@ export function useZap () {
try { try {
await abortSignal.pause({ me, amount: sats }) await abortSignal.pause({ me, amount: sats })
strike() animate()
// batch zaps if wallet is enabled or using fee credits so they can be executed serially in a single request // batch zaps if wallet is enabled or using fee credits so they can be executed serially in a single request
const { error } = await act({ variables, optimisticResponse, context: { batch: wallets.length > 0 || me?.privates?.sats > sats } }) const { error } = await act({ variables, optimisticResponse, context: { batch: wallets.length > 0 || me?.privates?.sats > sats } })
if (error) throw error if (error) throw error
@ -327,7 +327,7 @@ export function useZap () {
// but right now this toast is noisy for optimistic zaps // but right now this toast is noisy for optimistic zaps
console.error(error) console.error(error)
} }
}, [act, toaster, strike, wallets]) }, [act, toaster, animate, wallets])
} }
export class ActCanceledError extends Error { export class ActCanceledError extends Error {

View File

@ -14,8 +14,6 @@ import { abbrNum } from '../../lib/format'
import { useServiceWorker } from '../serviceworker' import { useServiceWorker } from '../serviceworker'
import { signOut } from 'next-auth/react' import { signOut } from 'next-auth/react'
import Badges from '../badge' import Badges from '../badge'
import { randInRange } from '../../lib/rand'
import { useFireworks } from '../fireworks'
import LightningIcon from '../../svgs/bolt.svg' import LightningIcon from '../../svgs/bolt.svg'
import SearchIcon from '../../svgs/search-line.svg' import SearchIcon from '../../svgs/search-line.svg'
import classNames from 'classnames' import classNames from 'classnames'
@ -400,20 +398,6 @@ export function LoginButtons ({ handleClose }) {
} }
export function AnonDropdown ({ path }) { export function AnonDropdown ({ path }) {
const strike = useFireworks()
useEffect(() => {
if (!window.localStorage.getItem('striked')) {
const to = setTimeout(() => {
const striked = strike()
if (striked) {
window.localStorage.setItem('striked', 'yep')
}
}, randInRange(3000, 10000))
return () => clearTimeout(to)
}
}, [])
return ( return (
<div className='position-relative'> <div className='position-relative'>
<Dropdown className={styles.dropdown} align='end' autoClose> <Dropdown className={styles.dropdown} align='end' autoClose>

View File

@ -6,7 +6,7 @@ import { numWithUnits } from '@/lib/format'
import { useShowModal } from './modal' import { useShowModal } from './modal'
import { useRoot } from './root' import { useRoot } from './root'
import { ActCanceledError, useAct } from './item-act' import { ActCanceledError, useAct } from './item-act'
import { useFireworks } from './fireworks' import { useAnimation } from '@/components/animation'
import { useToast } from './toast' import { useToast } from './toast'
import { useSendWallets } from '@/wallets/index' import { useSendWallets } from '@/wallets/index'
import { Form, SubmitButton } from './form' import { Form, SubmitButton } from './form'
@ -48,7 +48,7 @@ export default function PayBounty ({ children, item }) {
const { me } = useMe() const { me } = useMe()
const showModal = useShowModal() const showModal = useShowModal()
const root = useRoot() const root = useRoot()
const strike = useFireworks() const animate = useAnimation()
const toaster = useToast() const toaster = useToast()
const wallets = useSendWallets() const wallets = useSendWallets()
@ -61,7 +61,7 @@ export default function PayBounty ({ children, item }) {
const handlePayBounty = async onCompleted => { const handlePayBounty = async onCompleted => {
try { try {
strike() animate()
const { error } = await act({ onCompleted }) const { error } = await act({ onCompleted })
if (error) throw error if (error) throw error
} catch (error) { } catch (error) {

View File

@ -10,7 +10,7 @@ import { useRouter } from 'next/dist/client/router'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect } from 'react'
import { ShowModalProvider } from '@/components/modal' import { ShowModalProvider } from '@/components/modal'
import ErrorBoundary from '@/components/error-boundary' import ErrorBoundary from '@/components/error-boundary'
import { FireworksProvider } from '@/components/fireworks' import { AnimationProvider } from '@/components/animation'
import { ToastProvider } from '@/components/toast' import { ToastProvider } from '@/components/toast'
import { ServiceWorkerProvider } from '@/components/serviceworker' import { ServiceWorkerProvider } from '@/components/serviceworker'
import { SSR } from '@/lib/constants' import { SSR } from '@/lib/constants'
@ -116,7 +116,7 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
<WebLnProvider> <WebLnProvider>
<ServiceWorkerProvider> <ServiceWorkerProvider>
<PriceProvider price={price}> <PriceProvider price={price}>
<FireworksProvider> <AnimationProvider>
<ToastProvider> <ToastProvider>
<ShowModalProvider> <ShowModalProvider>
<BlockHeightProvider blockHeight={blockHeight}> <BlockHeightProvider blockHeight={blockHeight}>
@ -129,7 +129,7 @@ export default function MyApp ({ Component, pageProps: { ...props } }) {
</BlockHeightProvider> </BlockHeightProvider>
</ShowModalProvider> </ShowModalProvider>
</ToastProvider> </ToastProvider>
</FireworksProvider> </AnimationProvider>
</PriceProvider> </PriceProvider>
</ServiceWorkerProvider> </ServiceWorkerProvider>
</WebLnProvider> </WebLnProvider>

View File

@ -2,7 +2,7 @@ import { getGetServerSideProps } from '@/api/ssrApollo'
import CCInfo from '@/components/info/cc' import CCInfo from '@/components/info/cc'
import { Form, Input, SubmitButton } from '@/components/form' import { Form, Input, SubmitButton } from '@/components/form'
import { CenterLayout } from '@/components/layout' import { CenterLayout } from '@/components/layout'
import { useFireworks } from '@/components/fireworks' import { useAnimation } from '@/components/animation'
import { useMe } from '@/components/me' import { useMe } from '@/components/me'
import { useShowModal } from '@/components/modal' import { useShowModal } from '@/components/modal'
import { usePaidMutation } from '@/components/use-paid-mutation' import { usePaidMutation } from '@/components/use-paid-mutation'
@ -76,7 +76,7 @@ function WithdrawButton ({ className }) {
export function BuyCreditsButton ({ className }) { export function BuyCreditsButton ({ className }) {
const showModal = useShowModal() const showModal = useShowModal()
const strike = useFireworks() const animate = useAnimation()
const [buyCredits] = usePaidMutation(BUY_CREDITS) const [buyCredits] = usePaidMutation(BUY_CREDITS)
return ( return (
@ -94,7 +94,7 @@ export function BuyCreditsButton ({ className }) {
credits: Number(amount) credits: Number(amount)
}, },
onCompleted: () => { onCompleted: () => {
strike() animate()
} }
}) })
onClose() onClose()

View File

@ -13,7 +13,7 @@ import { useShowModal } from '@/components/modal'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants' import { FAST_POLL_INTERVAL, SSR } from '@/lib/constants'
import { useToast } from '@/components/toast' import { useToast } from '@/components/toast'
import { useFireworks } from '@/components/fireworks' import { useAnimation } from '@/components/animation'
import { Col, Row } from 'react-bootstrap' import { Col, Row } from 'react-bootstrap'
import { useData } from '@/components/use-data' import { useData } from '@/components/use-data'
import { GrowthPieChartSkeleton } from '@/components/charts-skeletons' import { GrowthPieChartSkeleton } from '@/components/charts-skeletons'
@ -133,7 +133,7 @@ export default function Rewards ({ ssrData }) {
export function DonateButton () { export function DonateButton () {
const showModal = useShowModal() const showModal = useShowModal()
const toaster = useToast() const toaster = useToast()
const strike = useFireworks() const animate = useAnimation()
const [donateToRewards] = usePaidMutation(DONATE) const [donateToRewards] = usePaidMutation(DONATE)
return ( return (
@ -151,7 +151,7 @@ export function DonateButton () {
sats: Number(amount) sats: Number(amount)
}, },
onCompleted: () => { onCompleted: () => {
strike() animate()
toaster.success('donated') toaster.success('donated')
} }
}) })