Animate gifts on hover

This commit is contained in:
Alexander Zinchuk 2025-08-15 18:25:35 +02:00
parent 3d60271505
commit e77c6c40f8
9 changed files with 61 additions and 19 deletions

View File

@ -50,6 +50,8 @@ export type OwnProps = {
sharedCanvas?: HTMLCanvasElement; sharedCanvas?: HTMLCanvasElement;
sharedCanvasCoords?: { x: number; y: number }; sharedCanvasCoords?: { x: number; y: number };
onClick?: NoneToVoidFunction; onClick?: NoneToVoidFunction;
onMouseEnter?: NoneToVoidFunction;
onMouseLeave?: NoneToVoidFunction;
onLoad?: NoneToVoidFunction; onLoad?: NoneToVoidFunction;
onEnded?: NoneToVoidFunction; onEnded?: NoneToVoidFunction;
onLoop?: NoneToVoidFunction; onLoop?: NoneToVoidFunction;
@ -76,6 +78,8 @@ const AnimatedSticker: FC<OwnProps> = ({
sharedCanvas, sharedCanvas,
sharedCanvasCoords, sharedCanvasCoords,
onClick, onClick,
onMouseEnter,
onMouseLeave,
onLoad, onLoad,
onEnded, onEnded,
onLoop, onLoop,
@ -277,6 +281,8 @@ const AnimatedSticker: FC<OwnProps> = ({
style, style,
)} )}
onClick={onClick} onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/> />
); );
}; };

View File

@ -6,6 +6,7 @@ import type { ApiEmojiStatusType, ApiPeer, ApiSavedStarGift } from '../../../api
import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config'; import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config';
import { getHasAdminRight } from '../../../global/helpers'; import { getHasAdminRight } from '../../../global/helpers';
import { selectChat, selectPeer, selectUser } from '../../../global/selectors'; import { selectChat, selectPeer, selectUser } from '../../../global/selectors';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment.ts';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format'; import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format';
import { CUSTOM_PEER_HIDDEN } from '../../../util/objects/customPeer'; import { CUSTOM_PEER_HIDDEN } from '../../../util/objects/customPeer';
@ -13,6 +14,7 @@ import { formatIntegerCompact } from '../../../util/textFormat';
import { getGiftAttributes, getStickerFromGift, getTotalGiftAvailability } from '../helpers/gifts'; import { getGiftAttributes, getStickerFromGift, getTotalGiftAvailability } from '../helpers/gifts';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useFlag from '../../../hooks/useFlag.ts';
import { type ObserveFn } from '../../../hooks/useIntersectionObserver'; import { type ObserveFn } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
@ -59,13 +61,13 @@ const SavedGift = ({
const { openGiftInfoModal } = getActions(); const { openGiftInfoModal } = getActions();
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();
const stickerRef = useRef<HTMLDivElement>(); const stickerRef = useRef<HTMLDivElement>();
const lang = useLang(); const lang = useLang();
const canManage = peerId === currentUserId || hasAdminRights; const [isHover, markHover, unmarkHover] = useFlag();
const canManage = peerId === currentUserId || hasAdminRights;
const totalIssued = getTotalGiftAvailability(gift.gift); const totalIssued = getTotalGiftAvailability(gift.gift);
const starGift = gift.gift; const starGift = gift.gift;
const starGiftUnique = starGift.type === 'starGiftUnique' ? starGift : undefined; const starGiftUnique = starGift.type === 'starGiftUnique' ? starGift : undefined;
@ -150,6 +152,8 @@ const SavedGift = ({
onClick={handleClick} onClick={handleClick}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onMouseDown={handleBeforeContextMenu} onMouseDown={handleBeforeContextMenu}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined}
> >
{radialPatternBackdrop} {radialPatternBackdrop}
{!radialPatternBackdrop && <Avatar className={styles.topIcon} peer={avatarPeer} size="micro" />} {!radialPatternBackdrop && <Avatar className={styles.topIcon} peer={avatarPeer} size="micro" />}
@ -161,12 +165,13 @@ const SavedGift = ({
> >
{sticker && ( {sticker && (
<StickerView <StickerView
observeIntersectionForPlaying={observeIntersection}
observeIntersectionForLoading={observeIntersection}
containerRef={stickerRef} containerRef={stickerRef}
sticker={sticker} sticker={sticker}
size={GIFT_STICKER_SIZE} size={GIFT_STICKER_SIZE}
shouldLoop={isHover}
shouldPreloadPreview shouldPreloadPreview
observeIntersectionForPlaying={observeIntersection}
observeIntersectionForLoading={observeIntersection}
/> />
)} )}

View File

@ -167,6 +167,7 @@
} }
.starGift { .starGift {
cursor: var(--custom-cursor, pointer);
width: 13.75rem; width: 13.75rem;
} }

View File

@ -1,9 +1,11 @@
import { memo, useRef } from '../../../lib/teact/teact'; import { memo, useRef } from '@teact';
import type { ApiStarGiftUnique } from '../../../api/types'; import type { ApiStarGiftUnique } from '../../../api/types';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment.ts';
import { getGiftAttributes } from '../../common/helpers/gifts'; import { getGiftAttributes } from '../../common/helpers/gifts';
import useFlag from '../../../hooks/useFlag.ts';
import { type ObserveFn } from '../../../hooks/useIntersectionObserver'; import { type ObserveFn } from '../../../hooks/useIntersectionObserver';
import RadialPatternBackground from '../../common/profile/RadialPatternBackground'; import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
@ -31,12 +33,16 @@ const WebPageUniqueGift = ({
backdrop, model, pattern, backdrop, model, pattern,
} = getGiftAttributes(gift)!; } = getGiftAttributes(gift)!;
const [isHover, markHover, unmarkHover] = useFlag();
const backgroundColors = [backdrop!.centerColor, backdrop!.edgeColor]; const backgroundColors = [backdrop!.centerColor, backdrop!.edgeColor];
return ( return (
<div <div
className={styles.root} className={styles.root}
onClick={onClick} onClick={onClick}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined}
> >
<div className={styles.backgroundWrapper}> <div className={styles.backgroundWrapper}>
<RadialPatternBackground <RadialPatternBackground
@ -51,6 +57,7 @@ const WebPageUniqueGift = ({
containerRef={stickerRef} containerRef={stickerRef}
sticker={model!.sticker} sticker={model!.sticker}
size={STAR_GIFT_STICKER_SIZE} size={STAR_GIFT_STICKER_SIZE}
shouldLoop={isHover}
observeIntersectionForPlaying={observeIntersectionForPlaying} observeIntersectionForPlaying={observeIntersectionForPlaying}
observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForLoading={observeIntersectionForLoading}
/> />

View File

@ -1,4 +1,4 @@
import { memo, useMemo, useRef } from '../../../../lib/teact/teact'; import { memo, useMemo, useRef } from '@teact';
import { withGlobal } from '../../../../global'; import { withGlobal } from '../../../../global';
import type { ApiMessage, ApiPeer } from '../../../../api/types'; import type { ApiMessage, ApiPeer } from '../../../../api/types';
@ -12,6 +12,7 @@ import {
selectSender, selectSender,
selectUser, selectUser,
} from '../../../../global/selectors'; } from '../../../../global/selectors';
import { IS_TOUCH_ENV } from '../../../../util/browser/windowEnvironment.ts';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
import { formatStarsAsText } from '../../../../util/localization/format'; import { formatStarsAsText } from '../../../../util/localization/format';
import { getServerTime } from '../../../../util/serverTime'; import { getServerTime } from '../../../../util/serverTime';
@ -21,6 +22,7 @@ import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEn
import { renderPeerLink, translateWithYou } from '../helpers/messageActions'; import { renderPeerLink, translateWithYou } from '../helpers/messageActions';
import useDynamicColorListener from '../../../../hooks/stickers/useDynamicColorListener'; import useDynamicColorListener from '../../../../hooks/stickers/useDynamicColorListener';
import useFlag from '../../../../hooks/useFlag.ts';
import { type ObserveFn } from '../../../../hooks/useIntersectionObserver'; import { type ObserveFn } from '../../../../hooks/useIntersectionObserver';
import useLang from '../../../../hooks/useLang'; import useLang from '../../../../hooks/useLang';
@ -62,6 +64,8 @@ const StarGiftAction = ({
const stickerRef = useRef<HTMLDivElement>(); const stickerRef = useRef<HTMLDivElement>();
const lang = useLang(); const lang = useLang();
const [isHover, markHover, unmarkHover] = useFlag();
const { isOutgoing } = message; const { isOutgoing } = message;
const sticker = getStickerFromGift(action.gift)!; const sticker = getStickerFromGift(action.gift)!;
@ -123,6 +127,8 @@ const StarGiftAction = ({
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={onClick} onClick={onClick}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined}
> >
<div <div
ref={stickerRef} ref={stickerRef}
@ -134,6 +140,7 @@ const StarGiftAction = ({
containerRef={stickerRef} containerRef={stickerRef}
sticker={sticker} sticker={sticker}
size={STICKER_SIZE} size={STICKER_SIZE}
shouldLoop={isHover}
observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying} observeIntersectionForPlaying={observeIntersectionForPlaying}
noLoad={!canPlayAnimatedEmojis} noLoad={!canPlayAnimatedEmojis}

View File

@ -1,4 +1,4 @@
import { memo, useMemo, useRef } from '../../../../lib/teact/teact'; import { memo, useMemo, useRef } from '@teact';
import { withGlobal } from '../../../../global'; import { withGlobal } from '../../../../global';
import type { ApiMessage, ApiPeer } from '../../../../api/types'; import type { ApiMessage, ApiPeer } from '../../../../api/types';
@ -11,11 +11,13 @@ import {
selectSender, selectSender,
selectUser, selectUser,
} from '../../../../global/selectors'; } from '../../../../global/selectors';
import { IS_TOUCH_ENV } from '../../../../util/browser/windowEnvironment.ts';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
import buildStyle from '../../../../util/buildStyle'; import buildStyle from '../../../../util/buildStyle';
import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts'; import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts';
import { renderPeerLink } from '../helpers/messageActions'; import { renderPeerLink } from '../helpers/messageActions';
import useFlag from '../../../../hooks/useFlag.ts';
import { type ObserveFn } from '../../../../hooks/useIntersectionObserver'; import { type ObserveFn } from '../../../../hooks/useIntersectionObserver';
import useLang from '../../../../hooks/useLang'; import useLang from '../../../../hooks/useLang';
@ -56,6 +58,8 @@ const StarGiftAction = ({
const stickerRef = useRef<HTMLDivElement>(); const stickerRef = useRef<HTMLDivElement>();
const lang = useLang(); const lang = useLang();
const [isHover, markHover, unmarkHover] = useFlag();
const { isOutgoing } = message; const { isOutgoing } = message;
const sticker = getStickerFromGift(action.gift)!; const sticker = getStickerFromGift(action.gift)!;
@ -85,6 +89,8 @@ const StarGiftAction = ({
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={onClick} onClick={onClick}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined}
> >
<div className={styles.uniqueBackgroundWrapper}> <div className={styles.uniqueBackgroundWrapper}>
<RadialPatternBackground <RadialPatternBackground
@ -105,6 +111,7 @@ const StarGiftAction = ({
containerRef={stickerRef} containerRef={stickerRef}
sticker={sticker} sticker={sticker}
size={STICKER_SIZE} size={STICKER_SIZE}
shouldLoop={isHover}
observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying} observeIntersectionForPlaying={observeIntersectionForPlaying}
noLoad={!canPlayAnimatedEmojis} noLoad={!canPlayAnimatedEmojis}

View File

@ -1,18 +1,16 @@
import { memo, useMemo, useRef, useState } from '../../../lib/teact/teact'; import { memo, useMemo, useRef, useState } from '@teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type { ApiStarGift, ApiTypeCurrencyAmount } from '../../../api/types';
ApiStarGift,
ApiTypeCurrencyAmount,
} from '../../../api/types';
import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config'; import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../config';
import { selectIsCurrentUserPremium } from '../../../global/selectors'; import { selectIsCurrentUserPremium } from '../../../global/selectors';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment.ts';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format'; import { formatStarsAsIcon, formatTonAsIcon } from '../../../util/localization/format';
import { getStickerFromGift } from '../../common/helpers/gifts'; import { getGiftAttributes, getStickerFromGift } from '../../common/helpers/gifts';
import { getGiftAttributes } from '../../common/helpers/gifts';
import useFlag from '../../../hooks/useFlag.ts';
import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver'; import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
@ -58,7 +56,9 @@ function GiftItemStar({
} }
const lang = useLang(); const lang = useLang();
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [isHover, markHover, unmarkHover] = useFlag();
const sticker = getStickerFromGift(gift); const sticker = getStickerFromGift(gift);
const isGiftUnique = gift.type === 'starGiftUnique'; const isGiftUnique = gift.type === 'starGiftUnique';
@ -175,6 +175,8 @@ function GiftItemStar({
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={handleGiftClick} onClick={handleGiftClick}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined}
> >
{radialPatternBackdrop} {radialPatternBackdrop}
@ -190,6 +192,7 @@ function GiftItemStar({
containerRef={stickerRef} containerRef={stickerRef}
sticker={sticker} sticker={sticker}
size={GIFT_STICKER_SIZE} size={GIFT_STICKER_SIZE}
shouldLoop={isHover}
shouldPreloadPreview shouldPreloadPreview
/> />
)} )}

View File

@ -1,8 +1,8 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '@teact';
import type React from '../../../lib/teact/teact';
import { import {
memo, useEffect, useMemo, useRef, useState, memo, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact'; } from '@teact';
import type React from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {

View File

@ -1,5 +1,5 @@
import type { TeactNode } from '../../../lib/teact/teact'; import type { TeactNode } from '@teact';
import { memo, useMemo } from '../../../lib/teact/teact'; import { memo, useMemo } from '@teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { import type {
@ -10,10 +10,12 @@ import type {
import { import {
formatStarsTransactionAmount, formatStarsTransactionAmount,
} from '../../../global/helpers/payments'; } from '../../../global/helpers/payments';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment.ts';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import buildStyle from '../../../util/buildStyle'; import buildStyle from '../../../util/buildStyle';
import { useTransitionActiveKey } from '../../../hooks/animations/useTransitionActiveKey'; import { useTransitionActiveKey } from '../../../hooks/animations/useTransitionActiveKey';
import useFlag from '../../../hooks/useFlag.ts';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker'; import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
@ -52,6 +54,7 @@ const UniqueGiftHeader = ({
} = getActions(); } = getActions();
const lang = useLang(); const lang = useLang();
const [isHover, markHover, unmarkHover] = useFlag();
const activeKey = useTransitionActiveKey([modelAttribute, backdropAttribute, patternAttribute]); const activeKey = useTransitionActiveKey([modelAttribute, backdropAttribute, patternAttribute]);
const subtitleColor = backdropAttribute?.textColor; const subtitleColor = backdropAttribute?.textColor;
@ -83,6 +86,9 @@ const UniqueGiftHeader = ({
className={styles.sticker} className={styles.sticker}
sticker={modelAttribute.sticker} sticker={modelAttribute.sticker}
size={STICKER_SIZE} size={STICKER_SIZE}
noLoop={!isHover}
onMouseEnter={!IS_TOUCH_ENV ? markHover : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkHover : undefined}
/> />
</Transition> </Transition>
{title && <h1 className={styles.title}>{title}</h1>} {title && <h1 className={styles.title}>{title}</h1>}