Sticker Set Cover: Fix pausing when hidden

This commit is contained in:
Alexander Zinchuk 2022-11-01 18:53:26 +01:00
parent b8573ebf35
commit 3bffde60cb
8 changed files with 88 additions and 125 deletions

View File

@ -29,13 +29,13 @@ type OwnProps<T> = {
noAnimate?: boolean; noAnimate?: boolean;
title?: string; title?: string;
className?: string; className?: string;
clickArg: T;
noContextMenu?: boolean; noContextMenu?: boolean;
isSavedMessages?: boolean; isSavedMessages?: boolean;
canViewSet?: boolean; canViewSet?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
observeIntersection: ObserveFn; observeIntersection: ObserveFn;
onClick?: (arg: OwnProps<T>['clickArg'], isSilent?: boolean, shouldSchedule?: boolean) => void; onClick?: (arg: OwnProps<T>['clickArg'], isSilent?: boolean, shouldSchedule?: boolean) => void;
clickArg: T;
onFaveClick?: (sticker: ApiSticker) => void; onFaveClick?: (sticker: ApiSticker) => void;
onUnfaveClick?: (sticker: ApiSticker) => void; onUnfaveClick?: (sticker: ApiSticker) => void;
onRemoveRecentClick?: (sticker: ApiSticker) => void; onRemoveRecentClick?: (sticker: ApiSticker) => void;

View File

@ -1,4 +1,4 @@
import React, { memo, useCallback, useMemo } from '../../lib/teact/teact'; import React, { memo, useCallback } from '../../lib/teact/teact';
import type { ApiSticker, ApiStickerSet } from '../../api/types'; import type { ApiSticker, ApiStickerSet } from '../../api/types';
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
@ -11,7 +11,6 @@ import useLang from '../../hooks/useLang';
import ListItem from '../ui/ListItem'; import ListItem from '../ui/ListItem';
import Button from '../ui/Button'; import Button from '../ui/Button';
import StickerSetCoverAnimated from '../middle/composer/StickerSetCoverAnimated';
import StickerSetCover from '../middle/composer/StickerSetCover'; import StickerSetCover from '../middle/composer/StickerSetCover';
import StickerButton from './StickerButton'; import StickerButton from './StickerButton';
@ -19,6 +18,7 @@ import './StickerSetCard.scss';
type OwnProps = { type OwnProps = {
stickerSet?: ApiStickerSet; stickerSet?: ApiStickerSet;
noAnimate?: boolean;
className?: string; className?: string;
observeIntersection: ObserveFn; observeIntersection: ObserveFn;
onClick: (value: ApiSticker) => void; onClick: (value: ApiSticker) => void;
@ -26,6 +26,7 @@ type OwnProps = {
const StickerSetCard: FC<OwnProps> = ({ const StickerSetCard: FC<OwnProps> = ({
stickerSet, stickerSet,
noAnimate,
className, className,
observeIntersection, observeIntersection,
onClick, onClick,
@ -38,7 +39,11 @@ const StickerSetCard: FC<OwnProps> = ({
if (firstSticker) onClick(firstSticker); if (firstSticker) onClick(firstSticker);
}, [firstSticker, onClick]); }, [firstSticker, onClick]);
const preview = useMemo(() => { if (!stickerSet || !stickerSet.stickers) {
return undefined;
}
function renderPreview() {
if (!stickerSet) return undefined; if (!stickerSet) return undefined;
if (stickerSet.hasThumbnail || !firstSticker) { if (stickerSet.hasThumbnail || !firstSticker) {
return ( return (
@ -47,18 +52,12 @@ const StickerSetCard: FC<OwnProps> = ({
color="translucent" color="translucent"
isRtl={lang.isRtl} isRtl={lang.isRtl}
> >
{stickerSet.isLottie ? ( <StickerSetCover
<StickerSetCoverAnimated stickerSet={stickerSet}
size={STICKER_SIZE_GENERAL_SETTINGS} size={STICKER_SIZE_GENERAL_SETTINGS}
stickerSet={stickerSet} noAnimate={noAnimate}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
/> />
) : (
<StickerSetCover
stickerSet={stickerSet}
observeIntersection={observeIntersection}
/>
)}
</Button> </Button>
); );
} else { } else {
@ -67,17 +66,14 @@ const StickerSetCard: FC<OwnProps> = ({
sticker={firstSticker} sticker={firstSticker}
size={STICKER_SIZE_GENERAL_SETTINGS} size={STICKER_SIZE_GENERAL_SETTINGS}
title={stickerSet.title} title={stickerSet.title}
noAnimate={noAnimate}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
clickArg={undefined}
noContextMenu noContextMenu
isCurrentUserPremium isCurrentUserPremium
clickArg={undefined}
/> />
); );
} }
}, [firstSticker, lang.isRtl, observeIntersection, stickerSet]);
if (!stickerSet || !stickerSet.stickers) {
return undefined;
} }
return ( return (
@ -87,7 +83,7 @@ const StickerSetCard: FC<OwnProps> = ({
inactive={!firstSticker} inactive={!firstSticker}
onClick={handleCardClick} onClick={handleCardClick}
> >
{preview} {renderPreview()}
<div className="multiline-menu-item"> <div className="multiline-menu-item">
<div className="title">{stickerSet.title}</div> <div className="title">{stickerSet.title}</div>
<div className="subtitle">{lang('StickerPack.StickerCount', stickerSet.count, 'i')}</div> <div className="subtitle">{lang('StickerPack.StickerCount', stickerSet.count, 'i')}</div>

View File

@ -33,7 +33,6 @@ import Button from '../../ui/Button';
import StickerButton from '../../common/StickerButton'; import StickerButton from '../../common/StickerButton';
import StickerSet from './StickerSet'; import StickerSet from './StickerSet';
import StickerSetCover from './StickerSetCover'; import StickerSetCover from './StickerSetCover';
import StickerSetCoverAnimated from './StickerSetCoverAnimated';
import './StickerPicker.scss'; import './StickerPicker.scss';
@ -50,7 +49,7 @@ type StateProps = {
addedCustomEmojiIds?: string[]; addedCustomEmojiIds?: string[];
recentCustomEmoji: ApiSticker[]; recentCustomEmoji: ApiSticker[];
featuredCustomEmojiIds?: string[]; featuredCustomEmojiIds?: string[];
shouldPlay?: boolean; canAnimate?: boolean;
isSavedMessages?: boolean; isSavedMessages?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
}; };
@ -68,7 +67,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
recentCustomEmoji, recentCustomEmoji,
stickerSetsById, stickerSetsById,
featuredCustomEmojiIds, featuredCustomEmojiIds,
shouldPlay, canAnimate,
isSavedMessages, isSavedMessages,
isCurrentUserPremium, isCurrentUserPremium,
onCustomEmojiSelect, onCustomEmojiSelect,
@ -199,14 +198,10 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
> >
{stickerSet.id === RECENT_SYMBOL_SET_ID ? ( {stickerSet.id === RECENT_SYMBOL_SET_ID ? (
<i className="icon-recent" /> <i className="icon-recent" />
) : stickerSet.isLottie ? (
<StickerSetCoverAnimated
stickerSet={stickerSet as ApiStickerSet}
observeIntersection={observeIntersectionForCovers}
/>
) : ( ) : (
<StickerSetCover <StickerSetCover
stickerSet={stickerSet as ApiStickerSet} stickerSet={stickerSet as ApiStickerSet}
noAnimate={!canAnimate || !loadAndPlay}
observeIntersection={observeIntersectionForCovers} observeIntersection={observeIntersectionForCovers}
/> />
)} )}
@ -220,11 +215,12 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
size={STICKER_SIZE_PICKER_HEADER} size={STICKER_SIZE_PICKER_HEADER}
title={stickerSet.title} title={stickerSet.title}
className={buttonClassName} className={buttonClassName}
noAnimate={!canAnimate || !loadAndPlay}
observeIntersection={observeIntersectionForCovers} observeIntersection={observeIntersectionForCovers}
onClick={selectStickerSet}
clickArg={index}
noContextMenu noContextMenu
isCurrentUserPremium isCurrentUserPremium
onClick={selectStickerSet}
clickArg={index}
/> />
); );
} }
@ -260,14 +256,14 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
<StickerSet <StickerSet
key={stickerSet.id} key={stickerSet.id}
stickerSet={stickerSet} stickerSet={stickerSet}
loadAndPlay={Boolean(shouldPlay && loadAndPlay)} loadAndPlay={Boolean(canAnimate && loadAndPlay)}
index={i} index={i}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1} shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
onStickerSelect={handleEmojiSelect}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
isCustomEmojiPicker isCustomEmojiPicker
isCurrentUserPremium={isCurrentUserPremium} isCurrentUserPremium={isCurrentUserPremium}
onStickerSelect={handleEmojiSelect}
/> />
))} ))}
</div> </div>
@ -288,7 +284,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
stickerSetsById: setsById, stickerSetsById: setsById,
addedCustomEmojiIds: global.customEmojis.added.setIds, addedCustomEmojiIds: global.customEmojis.added.setIds,
shouldPlay: global.settings.byKey.shouldLoopStickers, canAnimate: global.settings.byKey.shouldLoopStickers,
isSavedMessages, isSavedMessages,
isCurrentUserPremium: selectIsCurrentUserPremium(global), isCurrentUserPremium: selectIsCurrentUserPremium(global),
recentCustomEmoji, recentCustomEmoji,

View File

@ -35,7 +35,6 @@ import Button from '../../ui/Button';
import StickerButton from '../../common/StickerButton'; import StickerButton from '../../common/StickerButton';
import StickerSet from './StickerSet'; import StickerSet from './StickerSet';
import StickerSetCover from './StickerSetCover'; import StickerSetCover from './StickerSetCover';
import StickerSetCoverAnimated from './StickerSetCoverAnimated';
import PremiumIcon from '../../common/PremiumIcon'; import PremiumIcon from '../../common/PremiumIcon';
import './StickerPicker.scss'; import './StickerPicker.scss';
@ -56,7 +55,7 @@ type StateProps = {
premiumStickers: ApiSticker[]; premiumStickers: ApiSticker[];
stickerSetsById: Record<string, ApiStickerSet>; stickerSetsById: Record<string, ApiStickerSet>;
addedSetIds?: string[]; addedSetIds?: string[];
shouldPlay?: boolean; canAnimate?: boolean;
isSavedMessages?: boolean; isSavedMessages?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
}; };
@ -78,7 +77,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
premiumStickers, premiumStickers,
addedSetIds, addedSetIds,
stickerSetsById, stickerSetsById,
shouldPlay, canAnimate,
isSavedMessages, isSavedMessages,
isCurrentUserPremium, isCurrentUserPremium,
onStickerSelect, onStickerSelect,
@ -286,14 +285,10 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
<i className="icon-favorite" /> <i className="icon-favorite" />
) : stickerSet.id === CHAT_STICKER_SET_ID ? ( ) : stickerSet.id === CHAT_STICKER_SET_ID ? (
<Avatar chat={chat} size="small" /> <Avatar chat={chat} size="small" />
) : stickerSet.isLottie ? (
<StickerSetCoverAnimated
stickerSet={stickerSet as ApiStickerSet}
observeIntersection={observeIntersectionForCovers}
/>
) : ( ) : (
<StickerSetCover <StickerSetCover
stickerSet={stickerSet as ApiStickerSet} stickerSet={stickerSet as ApiStickerSet}
noAnimate={!canAnimate || !loadAndPlay}
observeIntersection={observeIntersectionForCovers} observeIntersection={observeIntersectionForCovers}
/> />
)} )}
@ -307,11 +302,12 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
size={STICKER_SIZE_PICKER_HEADER} size={STICKER_SIZE_PICKER_HEADER}
title={stickerSet.title} title={stickerSet.title}
className={buttonClassName} className={buttonClassName}
noAnimate={!canAnimate || !loadAndPlay}
observeIntersection={observeIntersectionForCovers} observeIntersection={observeIntersectionForCovers}
onClick={selectStickerSet}
clickArg={index}
noContextMenu noContextMenu
isCurrentUserPremium isCurrentUserPremium
onClick={selectStickerSet}
clickArg={index}
/> />
); );
} }
@ -350,17 +346,17 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
<StickerSet <StickerSet
key={stickerSet.id} key={stickerSet.id}
stickerSet={stickerSet} stickerSet={stickerSet}
loadAndPlay={Boolean(shouldPlay && loadAndPlay)} loadAndPlay={Boolean(canAnimate && loadAndPlay)}
index={i} index={i}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1} shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
favoriteStickers={favoriteStickers}
isSavedMessages={isSavedMessages}
isCurrentUserPremium={isCurrentUserPremium}
onStickerSelect={handleStickerSelect} onStickerSelect={handleStickerSelect}
onStickerUnfave={handleStickerUnfave} onStickerUnfave={handleStickerUnfave}
onStickerFave={handleStickerFave} onStickerFave={handleStickerFave}
onStickerRemoveRecent={handleRemoveRecentSticker} onStickerRemoveRecent={handleRemoveRecentSticker}
favoriteStickers={favoriteStickers}
isSavedMessages={isSavedMessages}
isCurrentUserPremium={isCurrentUserPremium}
/> />
))} ))}
</div> </div>
@ -388,7 +384,7 @@ export default memo(withGlobal<OwnProps>(
premiumStickers: premiumSet.stickers, premiumStickers: premiumSet.stickers,
stickerSetsById: setsById, stickerSetsById: setsById,
addedSetIds: added.setIds, addedSetIds: added.setIds,
shouldPlay: global.settings.byKey.shouldLoopStickers, canAnimate: global.settings.byKey.shouldLoopStickers,
isSavedMessages, isSavedMessages,
isCurrentUserPremium: selectIsCurrentUserPremium(global), isCurrentUserPremium: selectIsCurrentUserPremium(global),
}; };

View File

@ -166,15 +166,15 @@ const StickerSet: FC<OwnProps> = ({
size={itemSize} size={itemSize}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
noAnimate={!loadAndPlay} noAnimate={!loadAndPlay}
isSavedMessages={isSavedMessages}
canViewSet
isCurrentUserPremium={isCurrentUserPremium}
onClick={onStickerSelect} onClick={onStickerSelect}
clickArg={sticker} clickArg={sticker}
onUnfaveClick={stickerSet.id === FAVORITE_SYMBOL_SET_ID && favoriteStickerIdsSet?.has(sticker.id) onUnfaveClick={stickerSet.id === FAVORITE_SYMBOL_SET_ID && favoriteStickerIdsSet?.has(sticker.id)
? onStickerUnfave : undefined} ? onStickerUnfave : undefined}
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined} onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined} onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
isSavedMessages={isSavedMessages}
canViewSet
isCurrentUserPremium={isCurrentUserPremium}
/> />
))} ))}
{!isExpanded && stickerSet.count > itemsBeforeCutout && ( {!isExpanded && stickerSet.count > itemsBeforeCutout && (

View File

@ -0,0 +1,3 @@
.video {
width: 100%;
}

View File

@ -1,44 +1,73 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact'; import React, { memo, useRef } from '../../../lib/teact/teact';
import type { ApiStickerSet } from '../../../api/types'; import type { ApiStickerSet } from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { STICKER_SIZE_PICKER_HEADER } from '../../../config';
import { IS_WEBM_SUPPORTED } from '../../../util/environment'; import { IS_WEBM_SUPPORTED } from '../../../util/environment';
import { getFirstLetters } from '../../../util/textFormat'; import { getFirstLetters } from '../../../util/textFormat';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import buildClassName from '../../../util/buildClassName';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useMedia from '../../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import useMediaTransition from '../../../hooks/useMediaTransition'; import useMediaTransition from '../../../hooks/useMediaTransition';
import AnimatedSticker from '../../common/AnimatedSticker';
import OptimizedVideo from '../../ui/OptimizedVideo'; import OptimizedVideo from '../../ui/OptimizedVideo';
import styles from './StickerSetCover.module.scss';
type OwnProps = { type OwnProps = {
stickerSet: ApiStickerSet; stickerSet: ApiStickerSet;
size?: number;
noAnimate?: boolean;
observeIntersection: ObserveFn; observeIntersection: ObserveFn;
}; };
const StickerSetCover: FC<OwnProps> = ({ stickerSet, observeIntersection }) => { const StickerSetCover: FC<OwnProps> = ({
stickerSet,
size = STICKER_SIZE_PICKER_HEADER,
noAnimate,
observeIntersection,
}) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const { hasThumbnail, isLottie, isVideos: isVideo } = stickerSet;
const isIntersecting = useIsIntersecting(ref, observeIntersection); const isIntersecting = useIsIntersecting(ref, observeIntersection);
const mediaData = useMedia(stickerSet.hasThumbnail && `stickerSet${stickerSet.id}`, !isIntersecting); const mediaData = useMedia((hasThumbnail || isLottie) && `stickerSet${stickerSet.id}`, !isIntersecting);
const transitionClassNames = useMediaTransition(mediaData); const isReady = mediaData && (!isVideo || IS_WEBM_SUPPORTED);
const isVideo = stickerSet.isVideos; const transitionClassNames = useMediaTransition(isReady);
const firstLetters = useMemo(() => {
if ((isVideo && !IS_WEBM_SUPPORTED) || !mediaData) return getFirstLetters(stickerSet.title, 2);
return undefined;
}, [isVideo, mediaData, stickerSet.title]);
return ( return (
<div ref={ref} className="sticker-set-cover"> <div ref={ref} className="sticker-set-cover">
{firstLetters} {isReady ? (
{isVideo ? ( isLottie ? (
<OptimizedVideo canPlay src={mediaData} className={transitionClassNames} loop disablePictureInPicture /> <AnimatedSticker
className={transitionClassNames}
tgsUrl={mediaData}
size={size}
play={isIntersecting && !noAnimate}
/>
) : isVideo ? (
<OptimizedVideo
className={buildClassName(styles.video, transitionClassNames)}
src={mediaData}
canPlay={isIntersecting && !noAnimate}
loop
disablePictureInPicture
/>
) : (
<img
src={mediaData}
className={transitionClassNames}
alt=""
/>
)
) : ( ) : (
<img src={mediaData} className={transitionClassNames} alt="" /> getFirstLetters(stickerSet.title, 2)
)} )}
</div> </div>
); );

View File

@ -1,57 +0,0 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
import type { ApiStickerSet } from '../../../api/types';
import { STICKER_SIZE_PICKER_HEADER } from '../../../config';
import { getFirstLetters } from '../../../util/textFormat';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useMedia from '../../../hooks/useMedia';
import useMediaTransition from '../../../hooks/useMediaTransition';
import AnimatedSticker from '../../common/AnimatedSticker';
type OwnProps = {
size?: number;
stickerSet: ApiStickerSet;
observeIntersection: ObserveFn;
};
const StickerSetCoverAnimated: FC<OwnProps> = ({
size = STICKER_SIZE_PICKER_HEADER,
stickerSet,
observeIntersection,
}) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const isIntersecting = useIsIntersecting(ref, observeIntersection);
const mediaHash = `stickerSet${stickerSet.id}`;
const lottieData = useMedia(mediaHash, !isIntersecting);
const transitionClassNames = useMediaTransition(lottieData);
const firstLetters = useMemo(() => {
if (lottieData) return undefined;
return getFirstLetters(stickerSet.title, 2);
}, [lottieData, stickerSet.title]);
return (
<div ref={ref} className="sticker-set-cover">
{firstLetters}
{lottieData && (
<AnimatedSticker
size={size}
tgsUrl={lottieData}
className={transitionClassNames}
play={isIntersecting}
/>
)}
</div>
);
};
export default memo(StickerSetCoverAnimated);