From 922fc7da356062fc15b912aca9f5279c08164a3d Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 15 Aug 2025 18:25:40 +0200 Subject: [PATCH] InteractiveSparkles: Support in modals (#6120) --- src/components/common/Avatar.tsx | 3 + .../common/InteractiveSparkles.module.scss | 5 ++ src/components/common/InteractiveSparkles.tsx | 62 +++++++++++++++++++ .../modals/common/ParticlesHeader.tsx | 33 +++++----- .../modals/gift/GiftModal.module.scss | 9 +++ src/components/modals/gift/GiftModal.tsx | 22 ++++++- .../StarsTransactionModal.module.scss | 9 +++ .../transaction/StarsTransactionModal.tsx | 33 +++++++--- 8 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 src/components/common/InteractiveSparkles.module.scss create mode 100644 src/components/common/InteractiveSparkles.tsx diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 3321b8548..a69b90632 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -89,6 +89,7 @@ type OwnProps = { observeIntersection?: ObserveFn; onClick?: (e: ReactMouseEvent, hasMedia: boolean) => void; onContextMenu?: (e: React.MouseEvent) => void; + onMouseMove?: (e: React.MouseEvent) => void; }; const Avatar: FC = ({ @@ -115,6 +116,7 @@ const Avatar: FC = ({ asMessageBubble, onClick, onContextMenu, + onMouseMove, }) => { const { openStoryViewer } = getActions(); @@ -304,6 +306,7 @@ const Avatar: FC = ({ onClick={handleClick} onContextMenu={onContextMenu} onMouseDown={handleMouseDown} + onMouseMove={onMouseMove} >
{typeof content === 'string' ? renderText(content, [isBig ? 'hq_emoji' : 'emoji']) : content} diff --git a/src/components/common/InteractiveSparkles.module.scss b/src/components/common/InteractiveSparkles.module.scss new file mode 100644 index 000000000..45c5292c3 --- /dev/null +++ b/src/components/common/InteractiveSparkles.module.scss @@ -0,0 +1,5 @@ +.sparkles { + pointer-events: auto; + position: absolute; + top: 0; +} diff --git a/src/components/common/InteractiveSparkles.tsx b/src/components/common/InteractiveSparkles.tsx new file mode 100644 index 000000000..fa5863117 --- /dev/null +++ b/src/components/common/InteractiveSparkles.tsx @@ -0,0 +1,62 @@ +import { memo, useEffect, useLayoutEffect, useRef } from '../../lib/teact/teact'; + +import buildClassName from '../../util/buildClassName'; +import { PARTICLE_BURST_PARAMS, PARTICLE_COLORS, setupParticles } from '../../util/particles'; + +import styles from './InteractiveSparkles.module.scss'; + +interface OwnProps { + color?: 'purple' | 'gold' | 'blue'; + centerShift?: readonly [number, number]; + isDisabled?: boolean; + className?: string; + onRequestAnimation?: (animate: NoneToVoidFunction) => void; +} + +const DEFAULT_PARTICLE_PARAMS = { + centerShift: [0, -36] as const, +}; + +const InteractiveSparkles = ({ + color = 'purple', + centerShift = DEFAULT_PARTICLE_PARAMS.centerShift, + isDisabled, + className, + onRequestAnimation, +}: OwnProps) => { + const canvasRef = useRef(); + + useLayoutEffect(() => { + if (isDisabled) return undefined; + + return setupParticles(canvasRef.current!, { + color: PARTICLE_COLORS[`${color}Gradient`], + centerShift, + }); + }, [centerShift, color, isDisabled]); + + useEffect(() => { + if (!onRequestAnimation) return; + + const animate = () => { + if (isDisabled) return; + + setupParticles(canvasRef.current!, { + color: PARTICLE_COLORS[`${color}Gradient`], + centerShift, + ...PARTICLE_BURST_PARAMS, + }); + }; + + onRequestAnimation(animate); + }, [centerShift, color, isDisabled, onRequestAnimation]); + + return ( + + ); +}; + +export default memo(InteractiveSparkles); diff --git a/src/components/modals/common/ParticlesHeader.tsx b/src/components/modals/common/ParticlesHeader.tsx index 233b1c257..e8b683e53 100644 --- a/src/components/modals/common/ParticlesHeader.tsx +++ b/src/components/modals/common/ParticlesHeader.tsx @@ -1,14 +1,14 @@ import type { TeactNode } from '@teact'; -import { memo, useLayoutEffect, useRef } from '@teact'; +import { memo, useRef } from '@teact'; import type { ApiSticker } from '../../../api/types'; import buildClassName from '../../../util/buildClassName'; -import { PARTICLE_BURST_PARAMS, PARTICLE_COLORS, setupParticles } from '../../../util/particles.ts'; import { REM } from '../../common/helpers/mediaDimensions'; import useLastCallback from '../../../hooks/useLastCallback.ts'; +import InteractiveSparkles from '../../common/InteractiveSparkles'; import StickerView from '../../common/StickerView'; import SpeedingDiamond from './SpeedingDiamond.tsx'; import SwayingStar from './SwayingStar.tsx'; @@ -40,29 +40,26 @@ function ParticlesHeader({ isDisabled, className, }: OwnProps) { - const canvasRef = useRef(); const stickerRef = useRef(); - - useLayoutEffect(() => { - if (isDisabled) return undefined; - - return setupParticles(canvasRef.current!, { - color: PARTICLE_COLORS[`${color}Gradient`], - ...PARTICLE_PARAMS, - }); - }, [color, isDisabled]); + const triggerSparklesRef = useRef<(() => void) | undefined>(); const handleMouseMove = useLastCallback(() => { - setupParticles(canvasRef.current!, { - color: PARTICLE_COLORS[`${color}Gradient`], - ...PARTICLE_PARAMS, - ...PARTICLE_BURST_PARAMS, - }); + triggerSparklesRef.current?.(); + }); + + const handleRequestAnimation = useLastCallback((animate: NoneToVoidFunction) => { + triggerSparklesRef.current = animate; }); return (
- + {model === 'swaying-star' ? ( cb(), SCROLL_THROTTLE, true); @@ -104,6 +104,7 @@ const GiftModal: FC = ({ const [isGiftScreenHeaderForStarGifts, setIsGiftScreenHeaderForStarGifts] = useState(false); const [selectedCategory, setSelectedCategory] = useState('all'); + const triggerSparklesRef = useRef<(() => void) | undefined>(); const areAllGiftsDisallowed = useMemo(() => { if (!disallowedGifts) { @@ -357,15 +358,30 @@ const GiftModal: FC = ({ handleCloseModal(); }); + const handleAvatarMouseMove = useLastCallback(() => { + triggerSparklesRef.current?.(); + }); + + const handleRequestAnimation = useLastCallback((animate: NoneToVoidFunction) => { + triggerSparklesRef.current = animate; + }); + function renderMainScreen() { return (
+ -
{!isSelf && !chat && !disallowedGifts?.shouldDisallowPremiumGifts && ( <> diff --git a/src/components/modals/stars/transaction/StarsTransactionModal.module.scss b/src/components/modals/stars/transaction/StarsTransactionModal.module.scss index fb31737ae..c3a606b0c 100644 --- a/src/components/modals/stars/transaction/StarsTransactionModal.module.scss +++ b/src/components/modals/stars/transaction/StarsTransactionModal.module.scss @@ -135,3 +135,12 @@ width: 150px; height: 150px; } + +.avatar { + z-index: 2; + transition: transform 0.25s ease-out; + + &:hover { + transform: scale(1.1); + } +} diff --git a/src/components/modals/stars/transaction/StarsTransactionModal.tsx b/src/components/modals/stars/transaction/StarsTransactionModal.tsx index 62b06196a..3d7902242 100644 --- a/src/components/modals/stars/transaction/StarsTransactionModal.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionModal.tsx @@ -1,5 +1,5 @@ import type { FC } from '../../../../lib/teact/teact'; -import { memo, useMemo } from '../../../../lib/teact/teact'; +import { memo, useMemo, useRef } from '../../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../../global'; import type { @@ -39,6 +39,7 @@ import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker'; import Avatar from '../../../common/Avatar'; import Icon from '../../../common/icons/Icon'; import StarIcon from '../../../common/icons/StarIcon'; +import InteractiveSparkles from '../../../common/InteractiveSparkles'; import SafeLink from '../../../common/SafeLink'; import TableInfoModal, { type TableData } from '../../common/TableInfoModal'; import UniqueGiftHeader from '../../gift/UniqueGiftHeader'; @@ -46,7 +47,7 @@ import PaidMediaThumb from './PaidMediaThumb'; import styles from './StarsTransactionModal.module.scss'; -import StarsBackground from '../../../../assets/stars-bg.png'; +const AVATAR_SPARKLES_CENTER_SHIFT = [0, -50] as const; export type OwnProps = { modal: TabState['starsTransactionModal']; @@ -71,6 +72,7 @@ const StarsTransactionModal: FC = ({ const lang = useLang(); const oldLang = useOldLang(); const { transaction } = modal || {}; + const triggerSparklesRef = useRef<(() => void) | undefined>(); const handleOpenMedia = useLastCallback(() => { const media = transaction?.extendedMedia; @@ -82,6 +84,14 @@ const StarsTransactionModal: FC = ({ }); }); + const handleAvatarMouseMove = useLastCallback(() => { + triggerSparklesRef.current?.(); + }); + + const handleRequestAnimation = useLastCallback((animate: NoneToVoidFunction) => { + triggerSparklesRef.current = animate; + }); + const starModalData = useMemo(() => { if (!transaction) { return undefined; @@ -160,14 +170,20 @@ const StarsTransactionModal: FC = ({ /> )} {shouldDisplayAvatar && ( - + )} {!sticker && !transaction.isPostsSearch && ( - )} {Boolean(title) &&

{title}

} @@ -305,7 +321,8 @@ const StarsTransactionModal: FC = ({ tableData, footer, }; - }, [transaction, oldLang, lang, peer, canPlayAnimatedEmojis, topSticker, paidMessageCommission]); + }, [transaction, oldLang, lang, peer, canPlayAnimatedEmojis, topSticker, + paidMessageCommission, handleRequestAnimation]); const prevModalData = usePrevious(starModalData); const renderingModalData = prevModalData || starModalData;