Custom Emoji: Fixes, optimizations, and refactoring (#2095)
This commit is contained in:
parent
7712058fe4
commit
1fda24ab92
3
src/assets/square.svg
Normal file
3
src/assets/square.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg fill="white" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" xml:space="preserve">
|
||||||
|
<rect fill="#777777" fill-opacity="0.1" x="64" y="64" width="384" height="384" rx="50" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 255 B |
@ -3,8 +3,9 @@
|
|||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
width: var(--custom-emoji-size);
|
width: var(--custom-emoji-size);
|
||||||
height: var(--custom-emoji-size);
|
height: var(--custom-emoji-size);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&.with-grid-fix .media {
|
&.with-grid-fix .media, &.with-grid-fix .thumb {
|
||||||
width: calc(100% + 1px) !important;
|
width: calc(100% + 1px) !important;
|
||||||
height: calc(100% + 1px) !important;
|
height: calc(100% + 1px) !important;
|
||||||
vertical-align: baseline;
|
vertical-align: baseline;
|
||||||
@ -19,15 +20,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media {
|
.thumb {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticker {
|
.media {
|
||||||
width: var(--custom-emoji-size) !important;
|
width: var(--custom-emoji-size) !important;
|
||||||
height: var(--custom-emoji-size) !important;
|
height: var(--custom-emoji-size) !important;
|
||||||
display: flex !important;
|
|
||||||
|
|
||||||
:global(canvas) {
|
:global(canvas) {
|
||||||
width: var(--custom-emoji-size) !important;
|
width: var(--custom-emoji-size) !important;
|
||||||
|
|||||||
@ -6,26 +6,19 @@ import { getGlobal } from '../../global';
|
|||||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import { IS_WEBM_SUPPORTED } from '../../util/environment';
|
|
||||||
import renderText from './helpers/renderText';
|
|
||||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||||
import { hexToRgb } from '../../util/switchTheme';
|
import { hexToRgb } from '../../util/switchTheme';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { getStickerPreviewHash } from '../../global/helpers';
|
|
||||||
import { selectIsAlwaysHighPriorityEmoji, selectIsDefaultEmojiStatusPack } from '../../global/selectors';
|
|
||||||
import safePlay from '../../util/safePlay';
|
import safePlay from '../../util/safePlay';
|
||||||
|
import { selectIsDefaultEmojiStatusPack } from '../../global/selectors';
|
||||||
|
|
||||||
import useMedia from '../../hooks/useMedia';
|
|
||||||
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
|
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
|
||||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
|
||||||
import useThumbnail from '../../hooks/useThumbnail';
|
|
||||||
import useCustomEmoji from './hooks/useCustomEmoji';
|
import useCustomEmoji from './hooks/useCustomEmoji';
|
||||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
|
||||||
|
|
||||||
import AnimatedSticker from './AnimatedSticker';
|
import StickerView from './StickerView';
|
||||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
|
||||||
|
|
||||||
import styles from './CustomEmoji.module.scss';
|
import styles from './CustomEmoji.module.scss';
|
||||||
|
import svgPlaceholder from '../../assets/square.svg';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
documentId: string;
|
documentId: string;
|
||||||
@ -34,8 +27,9 @@ type OwnProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
loopLimit?: number;
|
loopLimit?: number;
|
||||||
withGridFix?: boolean;
|
withGridFix?: boolean;
|
||||||
withPreview?: boolean;
|
shouldPreloadPreview?: boolean;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
onClick?: NoneToVoidFunction;
|
onClick?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -43,35 +37,26 @@ const STICKER_SIZE = 24;
|
|||||||
|
|
||||||
const CustomEmoji: FC<OwnProps> = ({
|
const CustomEmoji: FC<OwnProps> = ({
|
||||||
documentId,
|
documentId,
|
||||||
children,
|
|
||||||
size = STICKER_SIZE,
|
size = STICKER_SIZE,
|
||||||
className,
|
className,
|
||||||
loopLimit,
|
loopLimit,
|
||||||
withGridFix,
|
withGridFix,
|
||||||
withPreview,
|
shouldPreloadPreview,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
|
observeIntersectionForPlaying,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
// 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);
|
||||||
|
|
||||||
// An alternative to `withGlobal` to avoid adding numerous global containers
|
// An alternative to `withGlobal` to avoid adding numerous global containers
|
||||||
const customEmoji = useCustomEmoji(documentId);
|
const customEmoji = useCustomEmoji(documentId);
|
||||||
const isUnsupportedVideo = customEmoji?.isVideo && !IS_WEBM_SUPPORTED;
|
useEnsureCustomEmoji(documentId);
|
||||||
const mediaHash = customEmoji && `sticker${customEmoji.id}`;
|
|
||||||
const mediaData = useMedia(mediaHash);
|
|
||||||
|
|
||||||
const shouldLoadPreview = !mediaData && (withPreview || isUnsupportedVideo);
|
|
||||||
const previewMediaHash = shouldLoadPreview && customEmoji && getStickerPreviewHash(customEmoji.id);
|
|
||||||
const previewMediaData = useMedia(previewMediaHash);
|
|
||||||
const thumbDataUri = useThumbnail(customEmoji);
|
|
||||||
|
|
||||||
const shouldDisplayPreview = Boolean(mediaData ? isUnsupportedVideo : previewMediaData);
|
|
||||||
const transitionClassNames = useMediaTransition(shouldDisplayPreview ? previewMediaData : mediaData);
|
|
||||||
|
|
||||||
const loopCountRef = useRef(0);
|
const loopCountRef = useRef(0);
|
||||||
const [shouldLoop, setShouldLoop] = useState(true);
|
const [shouldLoop, setShouldLoop] = useState(true);
|
||||||
const [customColor, setCustomColor] = useState<[number, number, number] | undefined>();
|
|
||||||
|
|
||||||
|
const [customColor, setCustomColor] = useState<[number, number, number] | undefined>();
|
||||||
const hasCustomColor = customEmoji && selectIsDefaultEmojiStatusPack(getGlobal(), customEmoji.stickerSetInfo);
|
const hasCustomColor = customEmoji && selectIsDefaultEmojiStatusPack(getGlobal(), customEmoji.stickerSetInfo);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -88,10 +73,6 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
setCustomColor([customColorRgb.r, customColorRgb.g, customColorRgb.b]);
|
setCustomColor([customColorRgb.r, customColorRgb.g, customColorRgb.b]);
|
||||||
}, [hasCustomColor]);
|
}, [hasCustomColor]);
|
||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
|
||||||
|
|
||||||
useEnsureCustomEmoji(documentId);
|
|
||||||
|
|
||||||
const handleVideoEnded = useCallback((e) => {
|
const handleVideoEnded = useCallback((e) => {
|
||||||
if (!loopLimit) return;
|
if (!loopLimit) return;
|
||||||
|
|
||||||
@ -117,53 +98,6 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [loopLimit]);
|
}, [loopLimit]);
|
||||||
|
|
||||||
function renderContent() {
|
|
||||||
if (!customEmoji || (!thumbDataUri && !mediaData)) {
|
|
||||||
return (children && renderText(children, ['emoji']));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mediaData && !previewMediaData) {
|
|
||||||
return (
|
|
||||||
<img className={styles.media} src={thumbDataUri} alt={customEmoji.emoji} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldDisplayPreview || isUnsupportedVideo || (!customEmoji.isVideo && !customEmoji.isLottie)) {
|
|
||||||
return (
|
|
||||||
<img className={styles.media} src={previewMediaData || mediaData} alt={customEmoji.emoji} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (customEmoji.isVideo) {
|
|
||||||
return (
|
|
||||||
<OptimizedVideo
|
|
||||||
canPlay={isIntersecting && shouldLoop}
|
|
||||||
className={styles.media}
|
|
||||||
src={mediaData}
|
|
||||||
playsInline
|
|
||||||
muted
|
|
||||||
loop={!loopLimit}
|
|
||||||
disablePictureInPicture
|
|
||||||
onEnded={handleVideoEnded}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AnimatedSticker
|
|
||||||
size={size}
|
|
||||||
key={mediaData}
|
|
||||||
className={styles.sticker}
|
|
||||||
tgsUrl={mediaData}
|
|
||||||
play={isIntersecting}
|
|
||||||
color={customColor}
|
|
||||||
noLoop={!shouldLoop}
|
|
||||||
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), customEmoji.stickerSetInfo)}
|
|
||||||
onLoop={handleStickerLoop}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -174,11 +108,29 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
'emoji',
|
'emoji',
|
||||||
hasCustomColor && 'custom-color',
|
hasCustomColor && 'custom-color',
|
||||||
withGridFix && styles.withGridFix,
|
withGridFix && styles.withGridFix,
|
||||||
...transitionClassNames,
|
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
{!customEmoji ? (
|
||||||
|
<img className={styles.thumb} src={svgPlaceholder} alt="Emoji" />
|
||||||
|
) : (
|
||||||
|
<StickerView
|
||||||
|
containerRef={ref}
|
||||||
|
sticker={customEmoji}
|
||||||
|
isSmall
|
||||||
|
size={size}
|
||||||
|
customColor={customColor}
|
||||||
|
thumbClassName={styles.thumb}
|
||||||
|
fullMediaClassName={styles.media}
|
||||||
|
shouldLoop={shouldLoop}
|
||||||
|
loopLimit={loopLimit}
|
||||||
|
shouldPreloadPreview={shouldPreloadPreview}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
|
onVideoEnded={handleVideoEnded}
|
||||||
|
onAnimatedStickerLoop={handleStickerLoop}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,33 +2,24 @@ import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useMemo, useRef,
|
memo, useCallback, useEffect, useMemo, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, getGlobal } from '../../global';
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
import type { ApiBotInlineMediaResult, ApiSticker } from '../../api/types';
|
import type { ApiBotInlineMediaResult, ApiSticker } from '../../api/types';
|
||||||
import { ApiMediaFormat } from '../../api/types';
|
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||||
import safePlay from '../../util/safePlay';
|
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||||
import { IS_TOUCH_ENV, IS_WEBM_SUPPORTED } from '../../util/environment';
|
|
||||||
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
|
|
||||||
import { getStickerPreviewHash } from '../../global/helpers';
|
|
||||||
|
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
import useMedia from '../../hooks/useMedia';
|
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
|
||||||
import useFlag from '../../hooks/useFlag';
|
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
||||||
import useThumbnail from '../../hooks/useThumbnail';
|
|
||||||
|
|
||||||
import AnimatedSticker from './AnimatedSticker';
|
import StickerView from './StickerView';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import Menu from '../ui/Menu';
|
import Menu from '../ui/Menu';
|
||||||
import MenuItem from '../ui/MenuItem';
|
import MenuItem from '../ui/MenuItem';
|
||||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
|
||||||
|
|
||||||
import './StickerButton.scss';
|
import './StickerButton.scss';
|
||||||
|
|
||||||
@ -72,31 +63,14 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const localMediaHash = `sticker${sticker.id}`;
|
const {
|
||||||
const stickerSelector = `sticker-button-${sticker.id}`;
|
id, isCustomEmoji, hasEffect: isPremium, stickerSetInfo,
|
||||||
|
} = sticker;
|
||||||
|
const isLocked = !isCurrentUserPremium && isPremium;
|
||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
const shouldLoad = isIntersecting;
|
||||||
const thumbDataUri = useThumbnail(sticker);
|
|
||||||
const previewBlobUrl = useMedia(getStickerPreviewHash(sticker.id), !isIntersecting, ApiMediaFormat.BlobUrl);
|
|
||||||
|
|
||||||
const shouldPlay = isIntersecting && !noAnimate;
|
const shouldPlay = isIntersecting && !noAnimate;
|
||||||
const lottieData = useMedia(sticker.isLottie && localMediaHash, !shouldPlay);
|
|
||||||
const [isLottieLoaded, markLoaded, unmarkLoaded] = useFlag(Boolean(lottieData));
|
|
||||||
const canLottiePlay = isLottieLoaded && shouldPlay;
|
|
||||||
const isVideo = sticker.isVideo && IS_WEBM_SUPPORTED;
|
|
||||||
const isCustomEmoji = sticker.isCustomEmoji;
|
|
||||||
const videoBlobUrl = useMedia(isVideo && localMediaHash, !shouldPlay, ApiMediaFormat.BlobUrl);
|
|
||||||
const canVideoPlay = Boolean(isVideo && videoBlobUrl && shouldPlay);
|
|
||||||
const isPremiumSticker = sticker.hasEffect;
|
|
||||||
const isLocked = !isCurrentUserPremium && isPremiumSticker;
|
|
||||||
|
|
||||||
const { transitionClassNames: previewTransitionClassNames } = useShowTransition(
|
|
||||||
Boolean(previewBlobUrl || canLottiePlay),
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
'slow',
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isContextMenuOpen, contextMenuPosition,
|
isContextMenuOpen, contextMenuPosition,
|
||||||
@ -125,24 +99,6 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
getMenuElement,
|
getMenuElement,
|
||||||
);
|
);
|
||||||
|
|
||||||
// To avoid flickering
|
|
||||||
useEffect(() => {
|
|
||||||
if (!shouldPlay) {
|
|
||||||
unmarkLoaded();
|
|
||||||
}
|
|
||||||
}, [unmarkLoaded, shouldPlay]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isVideo || !ref.current) return;
|
|
||||||
const video = ref.current.querySelector('video');
|
|
||||||
if (!video) return;
|
|
||||||
if (canVideoPlay) {
|
|
||||||
safePlay(video);
|
|
||||||
} else {
|
|
||||||
video.pause();
|
|
||||||
}
|
|
||||||
}, [isVideo, canVideoPlay]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isIntersecting) handleContextMenuClose();
|
if (!isIntersecting) handleContextMenuClose();
|
||||||
}, [handleContextMenuClose, isIntersecting]);
|
}, [handleContextMenuClose, isIntersecting]);
|
||||||
@ -189,8 +145,8 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
}, [clickArg, onClick]);
|
}, [clickArg, onClick]);
|
||||||
|
|
||||||
const handleOpenSet = useCallback(() => {
|
const handleOpenSet = useCallback(() => {
|
||||||
openStickerSet({ stickerSetInfo: sticker.stickerSetInfo });
|
openStickerSet({ stickerSetInfo });
|
||||||
}, [openStickerSet, sticker]);
|
}, [openStickerSet, stickerSetInfo]);
|
||||||
|
|
||||||
const shouldShowCloseButton = !IS_TOUCH_ENV && onRemoveRecentClick;
|
const shouldShowCloseButton = !IS_TOUCH_ENV && onRemoveRecentClick;
|
||||||
|
|
||||||
@ -198,15 +154,14 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
'StickerButton',
|
'StickerButton',
|
||||||
onClick && 'interactive',
|
onClick && 'interactive',
|
||||||
isCustomEmoji && 'custom-emoji',
|
isCustomEmoji && 'custom-emoji',
|
||||||
stickerSelector,
|
`sticker-button-${id}`,
|
||||||
className,
|
className,
|
||||||
);
|
);
|
||||||
|
|
||||||
const style = (thumbDataUri && !canLottiePlay && !canVideoPlay) ? `background-image: url('${thumbDataUri}');` : '';
|
|
||||||
|
|
||||||
const contextMenuItems = useMemo(() => {
|
const contextMenuItems = useMemo(() => {
|
||||||
|
if (noContextMenu || isCustomEmoji) return [];
|
||||||
|
|
||||||
const items: ReactNode[] = [];
|
const items: ReactNode[] = [];
|
||||||
if (noContextMenu || isCustomEmoji) return items;
|
|
||||||
|
|
||||||
if (onUnfaveClick) {
|
if (onUnfaveClick) {
|
||||||
items.push(
|
items.push(
|
||||||
@ -261,36 +216,21 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={fullClassName}
|
className={fullClassName}
|
||||||
title={title || (sticker?.emoji)}
|
title={title || (sticker?.emoji)}
|
||||||
style={style}
|
data-sticker-id={id}
|
||||||
data-sticker-id={sticker.id}
|
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
{!canLottiePlay && !canVideoPlay && (
|
<StickerView
|
||||||
// eslint-disable-next-line jsx-a11y/alt-text
|
containerRef={ref}
|
||||||
<img src={previewBlobUrl} className={previewTransitionClassNames} />
|
sticker={sticker}
|
||||||
)}
|
isSmall
|
||||||
{isVideo && (
|
size={size}
|
||||||
<OptimizedVideo
|
shouldLoop
|
||||||
canPlay={canVideoPlay}
|
shouldPreloadPreview
|
||||||
className={previewTransitionClassNames}
|
noLoad={!shouldLoad}
|
||||||
src={videoBlobUrl}
|
noPlay={!shouldPlay}
|
||||||
loop
|
/>
|
||||||
playsInline
|
|
||||||
disablePictureInPicture
|
|
||||||
muted
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{shouldPlay && lottieData && (
|
|
||||||
<AnimatedSticker
|
|
||||||
tgsUrl={lottieData}
|
|
||||||
play
|
|
||||||
size={size}
|
|
||||||
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo)}
|
|
||||||
onLoad={markLoaded}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isLocked && (
|
{isLocked && (
|
||||||
<div
|
<div
|
||||||
className="sticker-locked"
|
className="sticker-locked"
|
||||||
@ -298,7 +238,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
<i className="icon-lock-badge" />
|
<i className="icon-lock-badge" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{isPremiumSticker && !isLocked && (
|
{isPremium && !isLocked && (
|
||||||
<div className="sticker-premium">
|
<div className="sticker-premium">
|
||||||
<i className="icon-premium" />
|
<i className="icon-premium" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
21
src/components/common/StickerView.module.scss
Normal file
21
src/components/common/StickerView.module.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&:global(.closing) {
|
||||||
|
transition-delay: 150ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb, .media {
|
||||||
|
// @optimization
|
||||||
|
&:not(:global(.shown)) {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/components/common/StickerView.tsx
Normal file
145
src/components/common/StickerView.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
import React, { memo, useState } from '../../lib/teact/teact';
|
||||||
|
import { getGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
import type { ApiSticker } from '../../api/types';
|
||||||
|
|
||||||
|
import { IS_WEBM_SUPPORTED } from '../../util/environment';
|
||||||
|
import * as mediaLoader from '../../util/mediaLoader';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { getStickerPreviewHash } from '../../global/helpers';
|
||||||
|
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
|
||||||
|
|
||||||
|
import useMedia from '../../hooks/useMedia';
|
||||||
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
|
import useThumbnail from '../../hooks/useThumbnail';
|
||||||
|
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||||
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
|
||||||
|
import AnimatedSticker from './AnimatedSticker';
|
||||||
|
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||||
|
|
||||||
|
import styles from './StickerView.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
containerRef: React.RefObject<HTMLDivElement>;
|
||||||
|
sticker: ApiSticker;
|
||||||
|
thumbClassName?: string;
|
||||||
|
fullMediaHash?: string;
|
||||||
|
fullMediaClassName?: string;
|
||||||
|
isSmall?: boolean;
|
||||||
|
size?: number;
|
||||||
|
customColor?: [number, number, number];
|
||||||
|
loopLimit?: number;
|
||||||
|
shouldLoop?: boolean;
|
||||||
|
shouldPreloadPreview?: boolean;
|
||||||
|
observeIntersection?: ObserveFn;
|
||||||
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
|
noLoad?: boolean;
|
||||||
|
noPlay?: boolean;
|
||||||
|
cacheBuster?: number;
|
||||||
|
onVideoEnded?: AnyToVoidFunction;
|
||||||
|
onAnimatedStickerLoop?: AnyToVoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STICKER_SIZE = 24;
|
||||||
|
|
||||||
|
const StickerView: FC<OwnProps> = ({
|
||||||
|
containerRef,
|
||||||
|
sticker,
|
||||||
|
thumbClassName,
|
||||||
|
fullMediaHash,
|
||||||
|
fullMediaClassName,
|
||||||
|
isSmall,
|
||||||
|
size = STICKER_SIZE,
|
||||||
|
customColor,
|
||||||
|
loopLimit,
|
||||||
|
shouldLoop = false,
|
||||||
|
shouldPreloadPreview,
|
||||||
|
observeIntersection,
|
||||||
|
observeIntersectionForPlaying,
|
||||||
|
noLoad,
|
||||||
|
noPlay,
|
||||||
|
cacheBuster,
|
||||||
|
onVideoEnded,
|
||||||
|
onAnimatedStickerLoop,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
id, isLottie, stickerSetInfo, emoji,
|
||||||
|
} = sticker;
|
||||||
|
const isUnsupportedVideo = sticker.isVideo && !IS_WEBM_SUPPORTED;
|
||||||
|
const isVideo = sticker.isVideo && !isUnsupportedVideo;
|
||||||
|
const isStatic = !isLottie && !isVideo;
|
||||||
|
const previewMediaHash = getStickerPreviewHash(sticker.id);
|
||||||
|
|
||||||
|
const isIntersectingForLoad = useIsIntersecting(containerRef, observeIntersection);
|
||||||
|
const shouldLoad = isIntersectingForLoad && !noLoad;
|
||||||
|
const isIntersectingForPlaying = useIsIntersecting(containerRef, observeIntersectionForPlaying);
|
||||||
|
const shouldPlay = isIntersectingForPlaying && !noPlay;
|
||||||
|
|
||||||
|
const thumbDataUri = useThumbnail(sticker);
|
||||||
|
// Use preview instead of thumb but only if it's already loaded
|
||||||
|
const [preloadedPreviewData] = useState(mediaLoader.getFromMemory(previewMediaHash));
|
||||||
|
const thumbData = preloadedPreviewData || thumbDataUri;
|
||||||
|
|
||||||
|
const shouldForcePreview = isUnsupportedVideo || (isStatic && isSmall);
|
||||||
|
fullMediaHash ||= shouldForcePreview ? previewMediaHash : `sticker${id}`;
|
||||||
|
|
||||||
|
// If preloaded preview is forced, it will render as thumb, so no need to load it again
|
||||||
|
const shouldSkipFullMedia = Boolean(fullMediaHash === previewMediaHash && preloadedPreviewData);
|
||||||
|
|
||||||
|
const fullMediaData = useMedia(fullMediaHash, !shouldLoad || shouldSkipFullMedia, undefined, cacheBuster);
|
||||||
|
const [isPlayerReady, markPlayerReady] = useFlag(Boolean(isLottie && fullMediaData));
|
||||||
|
const isFullMediaReady = fullMediaData && (isStatic || isPlayerReady);
|
||||||
|
|
||||||
|
const fullMediaClassNames = useMediaTransition(isFullMediaReady);
|
||||||
|
const thumbClassNames = useMediaTransition(!isFullMediaReady);
|
||||||
|
|
||||||
|
// Preload preview for Message Input and local message
|
||||||
|
useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img
|
||||||
|
src={thumbData}
|
||||||
|
className={buildClassName(styles.thumb, thumbClassName, thumbClassNames)}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
{isLottie ? (
|
||||||
|
<AnimatedSticker
|
||||||
|
size={size}
|
||||||
|
key={fullMediaData}
|
||||||
|
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames)}
|
||||||
|
tgsUrl={fullMediaData}
|
||||||
|
play={shouldPlay}
|
||||||
|
color={customColor}
|
||||||
|
noLoop={!shouldLoop}
|
||||||
|
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
|
||||||
|
onLoad={markPlayerReady}
|
||||||
|
onLoop={onAnimatedStickerLoop}
|
||||||
|
/>
|
||||||
|
) : isVideo ? (
|
||||||
|
<OptimizedVideo
|
||||||
|
canPlay={shouldPlay && shouldLoop}
|
||||||
|
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames)}
|
||||||
|
src={fullMediaData}
|
||||||
|
playsInline
|
||||||
|
muted
|
||||||
|
loop={!loopLimit}
|
||||||
|
disablePictureInPicture
|
||||||
|
onPlay={markPlayerReady}
|
||||||
|
onEnded={onVideoEnded}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames)}
|
||||||
|
src={fullMediaData}
|
||||||
|
alt={emoji}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(StickerView);
|
||||||
@ -310,9 +310,7 @@ function processEntity(
|
|||||||
|
|
||||||
if (entity.type === ApiMessageEntityTypes.CustomEmoji) {
|
if (entity.type === ApiMessageEntityTypes.CustomEmoji) {
|
||||||
return (
|
return (
|
||||||
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix>
|
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix />
|
||||||
{renderNestedMessagePart()}
|
|
||||||
</CustomEmoji>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
@ -420,9 +418,7 @@ function processEntity(
|
|||||||
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
||||||
case ApiMessageEntityTypes.CustomEmoji:
|
case ApiMessageEntityTypes.CustomEmoji:
|
||||||
return (
|
return (
|
||||||
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix>
|
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix />
|
||||||
{renderNestedMessagePart()}
|
|
||||||
</CustomEmoji>
|
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
return renderNestedMessagePart();
|
return renderNestedMessagePart();
|
||||||
|
|||||||
@ -10,14 +10,10 @@ const handlers = new Set<AnyToVoidFunction>();
|
|||||||
let prevGlobal: GlobalState | undefined;
|
let prevGlobal: GlobalState | undefined;
|
||||||
|
|
||||||
addCallback((global: GlobalState) => {
|
addCallback((global: GlobalState) => {
|
||||||
const customEmojiById = global.customEmojis.byId;
|
if (global.customEmojis.byId !== prevGlobal?.customEmojis.byId) {
|
||||||
|
for (const handler of handlers) {
|
||||||
if (customEmojiById === prevGlobal?.customEmojis.byId) {
|
handler();
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (const handler of handlers) {
|
|
||||||
handler();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prevGlobal = global;
|
prevGlobal = global;
|
||||||
@ -30,13 +26,11 @@ export default function useCustomEmoji(documentId: string) {
|
|||||||
setCustomEmoji(getGlobal().customEmojis.byId[documentId]);
|
setCustomEmoji(getGlobal().customEmojis.byId[documentId]);
|
||||||
}, [documentId]);
|
}, [documentId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(handleGlobalChange, [documentId, handleGlobalChange]);
|
||||||
if (!documentId) return;
|
|
||||||
handleGlobalChange();
|
|
||||||
}, [documentId, handleGlobalChange]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (customEmoji) return undefined;
|
if (customEmoji) return undefined;
|
||||||
|
|
||||||
handlers.add(handleGlobalChange);
|
handlers.add(handleGlobalChange);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const CustomEmojiButton: FC<OwnProps> = ({
|
|||||||
onMouseDown={handleClick}
|
onMouseDown={handleClick}
|
||||||
title={emoji.emoji}
|
title={emoji.emoji}
|
||||||
>
|
>
|
||||||
<CustomEmoji documentId={emoji.id} size={CUSTOM_EMOJI_SIZE} withPreview />
|
<CustomEmoji documentId={emoji.id} size={CUSTOM_EMOJI_SIZE} shouldPreloadPreview />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,26 +1,29 @@
|
|||||||
import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types';
|
import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types';
|
||||||
import { getCustomEmojiPreviewMediaData } from '../../../../util/customEmojiManager';
|
import { getCustomEmojiPreviewMediaData } from '../../../../util/customEmojiManager';
|
||||||
|
import placeholderSrc from '../../../../assets/square.svg';
|
||||||
|
|
||||||
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
|
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
|
||||||
|
|
||||||
export function buildCustomEmojiHtml(emoji: ApiSticker) {
|
export function buildCustomEmojiHtml(emoji: ApiSticker) {
|
||||||
const mediaData = getCustomEmojiPreviewMediaData(emoji.id);
|
const mediaData = getCustomEmojiPreviewMediaData(emoji.id);
|
||||||
const src = mediaData && `src="${mediaData}"`;
|
|
||||||
return `<img
|
return `<img
|
||||||
class="custom-emoji emoji emoji-small"
|
class="custom-emoji emoji emoji-small${!mediaData ? ' placeholder' : ''}"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
alt="${emoji.emoji}"
|
alt="${emoji.emoji}"
|
||||||
data-document-id="${emoji.id}"
|
data-document-id="${emoji.id}"
|
||||||
${src} />`;
|
src="${mediaData || placeholderSrc}"
|
||||||
|
/>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) {
|
export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) {
|
||||||
const mediaData = getCustomEmojiPreviewMediaData(entity.documentId);
|
const mediaData = getCustomEmojiPreviewMediaData(entity.documentId);
|
||||||
const src = mediaData && `src="${mediaData}"`;
|
|
||||||
return `<img
|
return `<img
|
||||||
class="custom-emoji emoji emoji-small"
|
class="custom-emoji emoji emoji-small${!mediaData ? ' placeholder' : ''}"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
alt="${rawText}"
|
alt="${rawText}"
|
||||||
data-document-id="${entity.documentId}"
|
data-document-id="${entity.documentId}"
|
||||||
${src} />`;
|
src="${mediaData || placeholderSrc}"
|
||||||
|
/>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -919,7 +919,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
<CustomEmoji
|
<CustomEmoji
|
||||||
documentId={senderEmojiStatus.documentId}
|
documentId={senderEmojiStatus.documentId}
|
||||||
loopLimit={EMOJI_STATUS_LOOP_LIMIT}
|
loopLimit={EMOJI_STATUS_LOOP_LIMIT}
|
||||||
observeIntersection={observeIntersectionForAnimatedStickers}
|
observeIntersection={observeIntersectionForMedia}
|
||||||
|
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!asForwarded && !senderEmojiStatus && senderIsPremium && <PremiumIcon />}
|
{!asForwarded && !senderEmojiStatus && senderIsPremium && <PremiumIcon />}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import type { ApiMessage } from '../../../api/types';
|
|||||||
import { ApiMediaFormat } from '../../../api/types';
|
import { ApiMediaFormat } from '../../../api/types';
|
||||||
|
|
||||||
import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
|
import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
|
||||||
import { getMessageMediaFormat, getMessageMediaHash, getStickerPreviewHash } from '../../../global/helpers';
|
import { getMessageMediaHash } from '../../../global/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
@ -13,13 +13,11 @@ import { getActions } from '../../../global';
|
|||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
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 useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useThumbnail from '../../../hooks/useThumbnail';
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import StickerView from '../../common/StickerView';
|
||||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||||
import OptimizedVideo from '../../ui/OptimizedVideo';
|
|
||||||
|
|
||||||
import './Sticker.scss';
|
import './Sticker.scss';
|
||||||
|
|
||||||
@ -45,56 +43,27 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
const { showNotification, openStickerSet } = getActions();
|
const { showNotification, openStickerSet } = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
// 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 sticker = message.content.sticker!;
|
const sticker = message.content.sticker!;
|
||||||
const {
|
const { stickerSetInfo, isVideo, hasEffect } = sticker;
|
||||||
isLottie, stickerSetInfo, isVideo, hasEffect,
|
|
||||||
} = sticker;
|
|
||||||
const canDisplayVideo = IS_WEBM_SUPPORTED;
|
|
||||||
const isMemojiSticker = 'isMissing' in stickerSetInfo;
|
|
||||||
|
|
||||||
const [isPlayingEffect, startPlayingEffect, stopPlayingEffect] = useFlag();
|
const mediaHash = sticker.isPreloadedGlobally ? undefined : (
|
||||||
const shouldLoad = useIsIntersecting(ref, observeIntersection);
|
getMessageMediaHash(message, isVideo && !IS_WEBM_SUPPORTED ? 'pictogram' : 'inline')!
|
||||||
const shouldPlay = useIsIntersecting(ref, observeIntersectionForPlaying);
|
|
||||||
|
|
||||||
const mediaHash = sticker.isPreloadedGlobally ? `sticker${sticker.id}` : getMessageMediaHash(message, 'inline')!;
|
|
||||||
const mediaHashEffect = `sticker${sticker.id}?size=f`;
|
|
||||||
|
|
||||||
const previewMediaHash = isVideo && !canDisplayVideo && (
|
|
||||||
sticker.isPreloadedGlobally ? getStickerPreviewHash(sticker.id) : getMessageMediaHash(message, 'pictogram'));
|
|
||||||
const previewBlobUrl = useMedia(previewMediaHash);
|
|
||||||
const thumbDataUri = useThumbnail(sticker);
|
|
||||||
const previewUrl = previewBlobUrl || thumbDataUri;
|
|
||||||
|
|
||||||
const mediaData = useMedia(
|
|
||||||
mediaHash,
|
|
||||||
!shouldLoad,
|
|
||||||
getMessageMediaFormat(message, 'inline'),
|
|
||||||
lastSyncTime,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canLoad = useIsIntersecting(ref, observeIntersection);
|
||||||
|
const canPlay = useIsIntersecting(ref, observeIntersectionForPlaying);
|
||||||
|
const mediaHashEffect = `sticker${sticker.id}?size=f`;
|
||||||
const effectBlobUrl = useMedia(
|
const effectBlobUrl = useMedia(
|
||||||
mediaHashEffect,
|
mediaHashEffect,
|
||||||
!shouldLoad || !hasEffect,
|
!canLoad || !hasEffect,
|
||||||
ApiMediaFormat.BlobUrl,
|
ApiMediaFormat.BlobUrl,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
);
|
);
|
||||||
|
const [isPlayingEffect, startPlayingEffect, stopPlayingEffect] = useFlag();
|
||||||
const isMediaLoaded = Boolean(mediaData);
|
|
||||||
const [isLottieLoaded, markLottieLoaded] = useFlag(isMediaLoaded);
|
|
||||||
const isMediaReady = isLottie ? isLottieLoaded : isMediaLoaded;
|
|
||||||
const transitionClassNames = useMediaTransition(isMediaReady);
|
|
||||||
|
|
||||||
const { width, height } = getStickerDimensions(sticker);
|
|
||||||
const thumbClassName = buildClassName('thumbnail', !thumbDataUri && 'empty');
|
|
||||||
|
|
||||||
const stickerClassName = buildClassName(
|
|
||||||
'Sticker media-inner',
|
|
||||||
isMemojiSticker && 'inactive',
|
|
||||||
hasEffect && !message.isOutgoing && 'reversed',
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleEffectEnded = useCallback(() => {
|
const handleEffectEnded = useCallback(() => {
|
||||||
stopPlayingEffect();
|
stopPlayingEffect();
|
||||||
@ -102,11 +71,11 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
}, [onStopEffect, stopPlayingEffect]);
|
}, [onStopEffect, stopPlayingEffect]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasEffect && shouldPlay && shouldPlayEffect) {
|
if (hasEffect && canPlay && shouldPlayEffect) {
|
||||||
startPlayingEffect();
|
startPlayingEffect();
|
||||||
onPlayEffect?.();
|
onPlayEffect?.();
|
||||||
}
|
}
|
||||||
}, [hasEffect, shouldPlayEffect, onPlayEffect, shouldPlay, startPlayingEffect]);
|
}, [hasEffect, canPlay, onPlayEffect, shouldPlayEffect, startPlayingEffect]);
|
||||||
|
|
||||||
const openModal = useCallback(() => {
|
const openModal = useCallback(() => {
|
||||||
openStickerSet({
|
openStickerSet({
|
||||||
@ -132,50 +101,33 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
openModal();
|
openModal();
|
||||||
}, [hasEffect, isPlayingEffect, lang, onPlayEffect, openModal, showNotification, startPlayingEffect]);
|
}, [hasEffect, isPlayingEffect, lang, onPlayEffect, openModal, showNotification, startPlayingEffect]);
|
||||||
|
|
||||||
|
const isMemojiSticker = 'isMissing' in stickerSetInfo;
|
||||||
|
const { width, height } = getStickerDimensions(sticker);
|
||||||
|
const className = buildClassName(
|
||||||
|
'Sticker media-inner',
|
||||||
|
isMemojiSticker && 'inactive',
|
||||||
|
hasEffect && !message.isOutgoing && 'reversed',
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className={stickerClassName} onClick={!isMemojiSticker ? handleClick : undefined}>
|
<div
|
||||||
{(!isMediaReady || (isVideo && !canDisplayVideo)) && (
|
ref={ref}
|
||||||
<img
|
className={className}
|
||||||
src={previewUrl}
|
style={`width: ${width}px; height: ${height}px;`}
|
||||||
width={width}
|
onClick={!isMemojiSticker ? handleClick : undefined}
|
||||||
height={height}
|
>
|
||||||
alt=""
|
<StickerView
|
||||||
className={thumbClassName}
|
containerRef={ref}
|
||||||
/>
|
sticker={sticker}
|
||||||
)}
|
fullMediaHash={mediaHash}
|
||||||
{!isLottie && !isVideo && (
|
fullMediaClassName="full-media"
|
||||||
<img
|
size={width}
|
||||||
src={mediaData as string}
|
shouldLoop={shouldLoop}
|
||||||
width={width}
|
noLoad={!canLoad}
|
||||||
height={height}
|
noPlay={!canPlay}
|
||||||
alt=""
|
cacheBuster={lastSyncTime}
|
||||||
className={buildClassName('full-media', transitionClassNames)}
|
/>
|
||||||
/>
|
{hasEffect && canLoad && isPlayingEffect && (
|
||||||
)}
|
|
||||||
{isVideo && canDisplayVideo && isMediaReady && (
|
|
||||||
<OptimizedVideo
|
|
||||||
canPlay={shouldPlay}
|
|
||||||
src={mediaData as string}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
playsInline
|
|
||||||
disablePictureInPicture
|
|
||||||
loop={shouldLoop}
|
|
||||||
muted
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isLottie && isMediaLoaded && (
|
|
||||||
<AnimatedSticker
|
|
||||||
key={mediaHash}
|
|
||||||
className={buildClassName('full-media', transitionClassNames)}
|
|
||||||
tgsUrl={mediaData}
|
|
||||||
size={width}
|
|
||||||
play={shouldPlay}
|
|
||||||
noLoop={!shouldLoop}
|
|
||||||
onLoad={markLottieLoaded}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasEffect && shouldLoad && isPlayingEffect && (
|
|
||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
key={mediaHashEffect}
|
key={mediaHashEffect}
|
||||||
className="effect-sticker"
|
className="effect-sticker"
|
||||||
|
|||||||
@ -85,8 +85,7 @@ export function updateStickerSet(
|
|||||||
setIds = setIds.filter((id) => id !== stickerSetId);
|
setIds = setIds.filter((id) => id !== stickerSetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customEmojiById = isCustomEmoji && currentStickerSet.stickers
|
const customEmojiById = isCustomEmoji && update.stickers && buildCollectionByKey(update.stickers, 'id');
|
||||||
&& buildCollectionByKey(currentStickerSet.stickers, 'id');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
|
|||||||
@ -21,13 +21,18 @@ const updateLastRendered = throttle(() => {
|
|||||||
RENDER_HISTORY.clear();
|
RENDER_HISTORY.clear();
|
||||||
}, THROTTLE, false);
|
}, THROTTLE, false);
|
||||||
|
|
||||||
export default function useEnsureCustomEmoji(id: string) {
|
export function notifyCustomEmojiRender(emojiId: string) {
|
||||||
RENDER_HISTORY.add(id);
|
RENDER_HISTORY.add(emojiId);
|
||||||
updateLastRendered();
|
updateLastRendered();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useEnsureCustomEmoji(id: string) {
|
||||||
|
notifyCustomEmojiRender(id);
|
||||||
|
|
||||||
if (getGlobal().customEmojis.byId[id]) {
|
if (getGlobal().customEmojis.byId[id]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOAD_QUEUE.add(id);
|
LOAD_QUEUE.add(id);
|
||||||
loadFromQueue();
|
loadFromQueue();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
import { ApiMediaFormat } from '../api/types';
|
import { ApiMediaFormat } from '../api/types';
|
||||||
|
|
||||||
import { getStickerPreviewHash } from '../global/helpers';
|
import { getStickerPreviewHash } from '../global/helpers';
|
||||||
|
import { notifyCustomEmojiRender } from '../hooks/useEnsureCustomEmoji';
|
||||||
import * as mediaLoader from './mediaLoader';
|
import * as mediaLoader from './mediaLoader';
|
||||||
import { throttle } from './schedulers';
|
import { throttle } from './schedulers';
|
||||||
|
|
||||||
const DOM_PROCESS_THROTTLE = 500;
|
const DOM_PROCESS_THROTTLE = 500;
|
||||||
|
|
||||||
function processDomForCustomEmoji() {
|
function processDomForCustomEmoji() {
|
||||||
const emojis = document.querySelectorAll<HTMLImageElement>('img[data-document-id]:not([src])');
|
const emojis = document.querySelectorAll<HTMLImageElement>('.custom-emoji.placeholder');
|
||||||
emojis.forEach((emoji) => {
|
emojis.forEach((emoji) => {
|
||||||
const mediaHash = getStickerPreviewHash(emoji.dataset.documentId!);
|
const emojiId = emoji.dataset.documentId!;
|
||||||
|
const mediaHash = getStickerPreviewHash(emojiId);
|
||||||
const mediaData = mediaLoader.getFromMemory(mediaHash);
|
const mediaData = mediaLoader.getFromMemory(mediaHash);
|
||||||
if (mediaData) {
|
if (mediaData) {
|
||||||
emoji.src = mediaData;
|
emoji.src = mediaData;
|
||||||
|
emoji.classList.remove('placeholder');
|
||||||
|
|
||||||
|
notifyCustomEmojiRender(emojiId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -23,6 +28,7 @@ export function getCustomEmojiPreviewMediaData(emojiId: string) {
|
|||||||
const mediaHash = getStickerPreviewHash(emojiId);
|
const mediaHash = getStickerPreviewHash(emojiId);
|
||||||
const data = mediaLoader.getFromMemory(mediaHash);
|
const data = mediaLoader.getFromMemory(mediaHash);
|
||||||
if (data) {
|
if (data) {
|
||||||
|
notifyCustomEmojiRender(emojiId);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user