InteractiveSparkles: Support in modals (#6120)
This commit is contained in:
parent
88ebd32335
commit
922fc7da35
@ -89,6 +89,7 @@ type OwnProps = {
|
|||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
onClick?: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => void;
|
onClick?: (e: ReactMouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => void;
|
||||||
onContextMenu?: (e: React.MouseEvent) => void;
|
onContextMenu?: (e: React.MouseEvent) => void;
|
||||||
|
onMouseMove?: (e: React.MouseEvent) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Avatar: FC<OwnProps> = ({
|
const Avatar: FC<OwnProps> = ({
|
||||||
@ -115,6 +116,7 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
asMessageBubble,
|
asMessageBubble,
|
||||||
onClick,
|
onClick,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
|
onMouseMove,
|
||||||
}) => {
|
}) => {
|
||||||
const { openStoryViewer } = getActions();
|
const { openStoryViewer } = getActions();
|
||||||
|
|
||||||
@ -304,6 +306,7 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={onMouseMove}
|
||||||
>
|
>
|
||||||
<div className="inner">
|
<div className="inner">
|
||||||
{typeof content === 'string' ? renderText(content, [isBig ? 'hq_emoji' : 'emoji']) : content}
|
{typeof content === 'string' ? renderText(content, [isBig ? 'hq_emoji' : 'emoji']) : content}
|
||||||
|
|||||||
5
src/components/common/InteractiveSparkles.module.scss
Normal file
5
src/components/common/InteractiveSparkles.module.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.sparkles {
|
||||||
|
pointer-events: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
62
src/components/common/InteractiveSparkles.tsx
Normal file
62
src/components/common/InteractiveSparkles.tsx
Normal file
@ -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<HTMLCanvasElement>();
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className={buildClassName(styles.sparkles, className)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(InteractiveSparkles);
|
||||||
@ -1,14 +1,14 @@
|
|||||||
import type { TeactNode } from '@teact';
|
import type { TeactNode } from '@teact';
|
||||||
import { memo, useLayoutEffect, useRef } from '@teact';
|
import { memo, useRef } from '@teact';
|
||||||
|
|
||||||
import type { ApiSticker } from '../../../api/types';
|
import type { ApiSticker } from '../../../api/types';
|
||||||
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { PARTICLE_BURST_PARAMS, PARTICLE_COLORS, setupParticles } from '../../../util/particles.ts';
|
|
||||||
import { REM } from '../../common/helpers/mediaDimensions';
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
import useLastCallback from '../../../hooks/useLastCallback.ts';
|
import useLastCallback from '../../../hooks/useLastCallback.ts';
|
||||||
|
|
||||||
|
import InteractiveSparkles from '../../common/InteractiveSparkles';
|
||||||
import StickerView from '../../common/StickerView';
|
import StickerView from '../../common/StickerView';
|
||||||
import SpeedingDiamond from './SpeedingDiamond.tsx';
|
import SpeedingDiamond from './SpeedingDiamond.tsx';
|
||||||
import SwayingStar from './SwayingStar.tsx';
|
import SwayingStar from './SwayingStar.tsx';
|
||||||
@ -40,29 +40,26 @@ function ParticlesHeader({
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
className,
|
className,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>();
|
|
||||||
const stickerRef = useRef<HTMLDivElement>();
|
const stickerRef = useRef<HTMLDivElement>();
|
||||||
|
const triggerSparklesRef = useRef<(() => void) | undefined>();
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (isDisabled) return undefined;
|
|
||||||
|
|
||||||
return setupParticles(canvasRef.current!, {
|
|
||||||
color: PARTICLE_COLORS[`${color}Gradient`],
|
|
||||||
...PARTICLE_PARAMS,
|
|
||||||
});
|
|
||||||
}, [color, isDisabled]);
|
|
||||||
|
|
||||||
const handleMouseMove = useLastCallback(() => {
|
const handleMouseMove = useLastCallback(() => {
|
||||||
setupParticles(canvasRef.current!, {
|
triggerSparklesRef.current?.();
|
||||||
color: PARTICLE_COLORS[`${color}Gradient`],
|
});
|
||||||
...PARTICLE_PARAMS,
|
|
||||||
...PARTICLE_BURST_PARAMS,
|
const handleRequestAnimation = useLastCallback((animate: NoneToVoidFunction) => {
|
||||||
});
|
triggerSparklesRef.current = animate;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName(styles.root, className)}>
|
<div className={buildClassName(styles.root, className)}>
|
||||||
<canvas ref={canvasRef} className={styles.particles} />
|
<InteractiveSparkles
|
||||||
|
color={color}
|
||||||
|
centerShift={PARTICLE_PARAMS.centerShift}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
className={styles.particles}
|
||||||
|
onRequestAnimation={handleRequestAnimation}
|
||||||
|
/>
|
||||||
|
|
||||||
{model === 'swaying-star' ? (
|
{model === 'swaying-star' ? (
|
||||||
<SwayingStar
|
<SwayingStar
|
||||||
|
|||||||
@ -199,6 +199,15 @@
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
z-index: 2;
|
||||||
|
transition: transform 0.25s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.logoBackground {
|
.logoBackground {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
|
|||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
|
import InteractiveSparkles from '../../common/InteractiveSparkles';
|
||||||
import SafeLink from '../../common/SafeLink';
|
import SafeLink from '../../common/SafeLink';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
@ -45,8 +46,6 @@ import StarGiftCategoryList from './StarGiftCategoryList';
|
|||||||
|
|
||||||
import styles from './GiftModal.module.scss';
|
import styles from './GiftModal.module.scss';
|
||||||
|
|
||||||
import StarsBackground from '../../../assets/stars-bg.png';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
modal: TabState['giftModal'];
|
modal: TabState['giftModal'];
|
||||||
};
|
};
|
||||||
@ -68,6 +67,7 @@ type StateProps = {
|
|||||||
const AVATAR_SIZE = 100;
|
const AVATAR_SIZE = 100;
|
||||||
const INTERSECTION_THROTTLE = 200;
|
const INTERSECTION_THROTTLE = 200;
|
||||||
const SCROLL_THROTTLE = 200;
|
const SCROLL_THROTTLE = 200;
|
||||||
|
const AVATAR_SPARKLES_CENTER_SHIFT = [0, -50] as const;
|
||||||
|
|
||||||
const runThrottledForScroll = throttle((cb) => cb(), SCROLL_THROTTLE, true);
|
const runThrottledForScroll = throttle((cb) => cb(), SCROLL_THROTTLE, true);
|
||||||
|
|
||||||
@ -104,6 +104,7 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
|||||||
const [isGiftScreenHeaderForStarGifts, setIsGiftScreenHeaderForStarGifts] = useState(false);
|
const [isGiftScreenHeaderForStarGifts, setIsGiftScreenHeaderForStarGifts] = useState(false);
|
||||||
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<StarGiftCategory>('all');
|
const [selectedCategory, setSelectedCategory] = useState<StarGiftCategory>('all');
|
||||||
|
const triggerSparklesRef = useRef<(() => void) | undefined>();
|
||||||
|
|
||||||
const areAllGiftsDisallowed = useMemo(() => {
|
const areAllGiftsDisallowed = useMemo(() => {
|
||||||
if (!disallowedGifts) {
|
if (!disallowedGifts) {
|
||||||
@ -357,15 +358,30 @@ const GiftModal: FC<OwnProps & StateProps> = ({
|
|||||||
handleCloseModal();
|
handleCloseModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleAvatarMouseMove = useLastCallback(() => {
|
||||||
|
triggerSparklesRef.current?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRequestAnimation = useLastCallback((animate: NoneToVoidFunction) => {
|
||||||
|
triggerSparklesRef.current = animate;
|
||||||
|
});
|
||||||
|
|
||||||
function renderMainScreen() {
|
function renderMainScreen() {
|
||||||
return (
|
return (
|
||||||
<div ref={scrollerRef} className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
|
<div ref={scrollerRef} className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
|
||||||
<div className={styles.avatars}>
|
<div className={styles.avatars}>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
className={styles.avatar}
|
||||||
size={AVATAR_SIZE}
|
size={AVATAR_SIZE}
|
||||||
peer={peer}
|
peer={peer}
|
||||||
|
onMouseMove={handleAvatarMouseMove}
|
||||||
|
/>
|
||||||
|
<InteractiveSparkles
|
||||||
|
className={styles.logoBackground}
|
||||||
|
color="gold"
|
||||||
|
centerShift={AVATAR_SPARKLES_CENTER_SHIFT}
|
||||||
|
onRequestAnimation={handleRequestAnimation}
|
||||||
/>
|
/>
|
||||||
<img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />
|
|
||||||
</div>
|
</div>
|
||||||
{!isSelf && !chat && !disallowedGifts?.shouldDisallowPremiumGifts && (
|
{!isSelf && !chat && !disallowedGifts?.shouldDisallowPremiumGifts && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -135,3 +135,12 @@
|
|||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
z-index: 2;
|
||||||
|
transition: transform 0.25s ease-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { FC } from '../../../../lib/teact/teact';
|
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 { getActions, withGlobal } from '../../../../global';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -39,6 +39,7 @@ import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
|
|||||||
import Avatar from '../../../common/Avatar';
|
import Avatar from '../../../common/Avatar';
|
||||||
import Icon from '../../../common/icons/Icon';
|
import Icon from '../../../common/icons/Icon';
|
||||||
import StarIcon from '../../../common/icons/StarIcon';
|
import StarIcon from '../../../common/icons/StarIcon';
|
||||||
|
import InteractiveSparkles from '../../../common/InteractiveSparkles';
|
||||||
import SafeLink from '../../../common/SafeLink';
|
import SafeLink from '../../../common/SafeLink';
|
||||||
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
|
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
|
||||||
import UniqueGiftHeader from '../../gift/UniqueGiftHeader';
|
import UniqueGiftHeader from '../../gift/UniqueGiftHeader';
|
||||||
@ -46,7 +47,7 @@ import PaidMediaThumb from './PaidMediaThumb';
|
|||||||
|
|
||||||
import styles from './StarsTransactionModal.module.scss';
|
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 = {
|
export type OwnProps = {
|
||||||
modal: TabState['starsTransactionModal'];
|
modal: TabState['starsTransactionModal'];
|
||||||
@ -71,6 +72,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
|||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const oldLang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
const { transaction } = modal || {};
|
const { transaction } = modal || {};
|
||||||
|
const triggerSparklesRef = useRef<(() => void) | undefined>();
|
||||||
|
|
||||||
const handleOpenMedia = useLastCallback(() => {
|
const handleOpenMedia = useLastCallback(() => {
|
||||||
const media = transaction?.extendedMedia;
|
const media = transaction?.extendedMedia;
|
||||||
@ -82,6 +84,14 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleAvatarMouseMove = useLastCallback(() => {
|
||||||
|
triggerSparklesRef.current?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRequestAnimation = useLastCallback((animate: NoneToVoidFunction) => {
|
||||||
|
triggerSparklesRef.current = animate;
|
||||||
|
});
|
||||||
|
|
||||||
const starModalData = useMemo(() => {
|
const starModalData = useMemo(() => {
|
||||||
if (!transaction) {
|
if (!transaction) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -160,14 +170,20 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{shouldDisplayAvatar && (
|
{shouldDisplayAvatar && (
|
||||||
<Avatar peer={avatarPeer} webPhoto={photo} size="giant" />
|
<Avatar
|
||||||
|
className={styles.avatar}
|
||||||
|
peer={avatarPeer}
|
||||||
|
webPhoto={photo}
|
||||||
|
size="giant"
|
||||||
|
onMouseMove={handleAvatarMouseMove}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{!sticker && !transaction.isPostsSearch && (
|
{!sticker && !transaction.isPostsSearch && (
|
||||||
<img
|
<InteractiveSparkles
|
||||||
className={buildClassName(styles.starsBackground)}
|
className={buildClassName(styles.starsBackground)}
|
||||||
src={StarsBackground}
|
color="gold"
|
||||||
alt=""
|
onRequestAnimation={handleRequestAnimation}
|
||||||
draggable={false}
|
centerShift={AVATAR_SPARKLES_CENTER_SHIFT}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{Boolean(title) && <h1 className={styles.title}>{title}</h1>}
|
{Boolean(title) && <h1 className={styles.title}>{title}</h1>}
|
||||||
@ -305,7 +321,8 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
|||||||
tableData,
|
tableData,
|
||||||
footer,
|
footer,
|
||||||
};
|
};
|
||||||
}, [transaction, oldLang, lang, peer, canPlayAnimatedEmojis, topSticker, paidMessageCommission]);
|
}, [transaction, oldLang, lang, peer, canPlayAnimatedEmojis, topSticker,
|
||||||
|
paidMessageCommission, handleRequestAnimation]);
|
||||||
|
|
||||||
const prevModalData = usePrevious(starModalData);
|
const prevModalData = usePrevious(starModalData);
|
||||||
const renderingModalData = prevModalData || starModalData;
|
const renderingModalData = prevModalData || starModalData;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user