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;
|
||||
width: 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;
|
||||
height: calc(100% + 1px) !important;
|
||||
vertical-align: baseline;
|
||||
@ -19,15 +20,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
.thumb {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sticker {
|
||||
.media {
|
||||
width: var(--custom-emoji-size) !important;
|
||||
height: var(--custom-emoji-size) !important;
|
||||
display: flex !important;
|
||||
|
||||
:global(canvas) {
|
||||
width: var(--custom-emoji-size) !important;
|
||||
|
||||
@ -6,26 +6,19 @@ import { getGlobal } from '../../global';
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import { IS_WEBM_SUPPORTED } from '../../util/environment';
|
||||
import renderText from './helpers/renderText';
|
||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||
import { hexToRgb } from '../../util/switchTheme';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { getStickerPreviewHash } from '../../global/helpers';
|
||||
import { selectIsAlwaysHighPriorityEmoji, selectIsDefaultEmojiStatusPack } from '../../global/selectors';
|
||||
import safePlay from '../../util/safePlay';
|
||||
import { selectIsDefaultEmojiStatusPack } from '../../global/selectors';
|
||||
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useThumbnail from '../../hooks/useThumbnail';
|
||||
import useCustomEmoji from './hooks/useCustomEmoji';
|
||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||
|
||||
import AnimatedSticker from './AnimatedSticker';
|
||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||
import StickerView from './StickerView';
|
||||
|
||||
import styles from './CustomEmoji.module.scss';
|
||||
import svgPlaceholder from '../../assets/square.svg';
|
||||
|
||||
type OwnProps = {
|
||||
documentId: string;
|
||||
@ -34,8 +27,9 @@ type OwnProps = {
|
||||
className?: string;
|
||||
loopLimit?: number;
|
||||
withGridFix?: boolean;
|
||||
withPreview?: boolean;
|
||||
shouldPreloadPreview?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
@ -43,35 +37,26 @@ const STICKER_SIZE = 24;
|
||||
|
||||
const CustomEmoji: FC<OwnProps> = ({
|
||||
documentId,
|
||||
children,
|
||||
size = STICKER_SIZE,
|
||||
className,
|
||||
loopLimit,
|
||||
withGridFix,
|
||||
withPreview,
|
||||
shouldPreloadPreview,
|
||||
observeIntersection,
|
||||
observeIntersectionForPlaying,
|
||||
onClick,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
// An alternative to `withGlobal` to avoid adding numerous global containers
|
||||
const customEmoji = useCustomEmoji(documentId);
|
||||
const isUnsupportedVideo = customEmoji?.isVideo && !IS_WEBM_SUPPORTED;
|
||||
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);
|
||||
useEnsureCustomEmoji(documentId);
|
||||
|
||||
const loopCountRef = useRef(0);
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@ -88,10 +73,6 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
setCustomColor([customColorRgb.r, customColorRgb.g, customColorRgb.b]);
|
||||
}, [hasCustomColor]);
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
|
||||
useEnsureCustomEmoji(documentId);
|
||||
|
||||
const handleVideoEnded = useCallback((e) => {
|
||||
if (!loopLimit) return;
|
||||
|
||||
@ -117,53 +98,6 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
}
|
||||
}, [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 (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -174,11 +108,29 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
'emoji',
|
||||
hasCustomColor && 'custom-color',
|
||||
withGridFix && styles.withGridFix,
|
||||
...transitionClassNames,
|
||||
)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -2,33 +2,24 @@ import type { MouseEvent as ReactMouseEvent, ReactNode } from 'react';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../global';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { ApiBotInlineMediaResult, ApiSticker } from '../../api/types';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||
import safePlay from '../../util/safePlay';
|
||||
import { IS_TOUCH_ENV, IS_WEBM_SUPPORTED } from '../../util/environment';
|
||||
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
|
||||
import { getStickerPreviewHash } from '../../global/helpers';
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
|
||||
import type { ObserveFn } 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 useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
||||
import useThumbnail from '../../hooks/useThumbnail';
|
||||
|
||||
import AnimatedSticker from './AnimatedSticker';
|
||||
import StickerView from './StickerView';
|
||||
import Button from '../ui/Button';
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||
|
||||
import './StickerButton.scss';
|
||||
|
||||
@ -72,31 +63,14 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
|
||||
const localMediaHash = `sticker${sticker.id}`;
|
||||
const stickerSelector = `sticker-button-${sticker.id}`;
|
||||
const {
|
||||
id, isCustomEmoji, hasEffect: isPremium, stickerSetInfo,
|
||||
} = sticker;
|
||||
const isLocked = !isCurrentUserPremium && isPremium;
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
|
||||
const thumbDataUri = useThumbnail(sticker);
|
||||
const previewBlobUrl = useMedia(getStickerPreviewHash(sticker.id), !isIntersecting, ApiMediaFormat.BlobUrl);
|
||||
|
||||
const shouldLoad = isIntersecting;
|
||||
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 {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
@ -125,24 +99,6 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
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(() => {
|
||||
if (!isIntersecting) handleContextMenuClose();
|
||||
}, [handleContextMenuClose, isIntersecting]);
|
||||
@ -189,8 +145,8 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
}, [clickArg, onClick]);
|
||||
|
||||
const handleOpenSet = useCallback(() => {
|
||||
openStickerSet({ stickerSetInfo: sticker.stickerSetInfo });
|
||||
}, [openStickerSet, sticker]);
|
||||
openStickerSet({ stickerSetInfo });
|
||||
}, [openStickerSet, stickerSetInfo]);
|
||||
|
||||
const shouldShowCloseButton = !IS_TOUCH_ENV && onRemoveRecentClick;
|
||||
|
||||
@ -198,15 +154,14 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
'StickerButton',
|
||||
onClick && 'interactive',
|
||||
isCustomEmoji && 'custom-emoji',
|
||||
stickerSelector,
|
||||
`sticker-button-${id}`,
|
||||
className,
|
||||
);
|
||||
|
||||
const style = (thumbDataUri && !canLottiePlay && !canVideoPlay) ? `background-image: url('${thumbDataUri}');` : '';
|
||||
|
||||
const contextMenuItems = useMemo(() => {
|
||||
if (noContextMenu || isCustomEmoji) return [];
|
||||
|
||||
const items: ReactNode[] = [];
|
||||
if (noContextMenu || isCustomEmoji) return items;
|
||||
|
||||
if (onUnfaveClick) {
|
||||
items.push(
|
||||
@ -261,36 +216,21 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
ref={ref}
|
||||
className={fullClassName}
|
||||
title={title || (sticker?.emoji)}
|
||||
style={style}
|
||||
data-sticker-id={sticker.id}
|
||||
data-sticker-id={id}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{!canLottiePlay && !canVideoPlay && (
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
<img src={previewBlobUrl} className={previewTransitionClassNames} />
|
||||
)}
|
||||
{isVideo && (
|
||||
<OptimizedVideo
|
||||
canPlay={canVideoPlay}
|
||||
className={previewTransitionClassNames}
|
||||
src={videoBlobUrl}
|
||||
loop
|
||||
playsInline
|
||||
disablePictureInPicture
|
||||
muted
|
||||
/>
|
||||
)}
|
||||
{shouldPlay && lottieData && (
|
||||
<AnimatedSticker
|
||||
tgsUrl={lottieData}
|
||||
play
|
||||
size={size}
|
||||
isLowPriority={!selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo)}
|
||||
onLoad={markLoaded}
|
||||
/>
|
||||
)}
|
||||
<StickerView
|
||||
containerRef={ref}
|
||||
sticker={sticker}
|
||||
isSmall
|
||||
size={size}
|
||||
shouldLoop
|
||||
shouldPreloadPreview
|
||||
noLoad={!shouldLoad}
|
||||
noPlay={!shouldPlay}
|
||||
/>
|
||||
{isLocked && (
|
||||
<div
|
||||
className="sticker-locked"
|
||||
@ -298,7 +238,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
<i className="icon-lock-badge" />
|
||||
</div>
|
||||
)}
|
||||
{isPremiumSticker && !isLocked && (
|
||||
{isPremium && !isLocked && (
|
||||
<div className="sticker-premium">
|
||||
<i className="icon-premium" />
|
||||
</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) {
|
||||
return (
|
||||
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix>
|
||||
{renderNestedMessagePart()}
|
||||
</CustomEmoji>
|
||||
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix />
|
||||
);
|
||||
}
|
||||
return text;
|
||||
@ -420,9 +418,7 @@ function processEntity(
|
||||
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
||||
case ApiMessageEntityTypes.CustomEmoji:
|
||||
return (
|
||||
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix>
|
||||
{renderNestedMessagePart()}
|
||||
</CustomEmoji>
|
||||
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection} withGridFix />
|
||||
);
|
||||
default:
|
||||
return renderNestedMessagePart();
|
||||
|
||||
@ -10,14 +10,10 @@ const handlers = new Set<AnyToVoidFunction>();
|
||||
let prevGlobal: GlobalState | undefined;
|
||||
|
||||
addCallback((global: GlobalState) => {
|
||||
const customEmojiById = global.customEmojis.byId;
|
||||
|
||||
if (customEmojiById === prevGlobal?.customEmojis.byId) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const handler of handlers) {
|
||||
handler();
|
||||
if (global.customEmojis.byId !== prevGlobal?.customEmojis.byId) {
|
||||
for (const handler of handlers) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
|
||||
prevGlobal = global;
|
||||
@ -30,13 +26,11 @@ export default function useCustomEmoji(documentId: string) {
|
||||
setCustomEmoji(getGlobal().customEmojis.byId[documentId]);
|
||||
}, [documentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!documentId) return;
|
||||
handleGlobalChange();
|
||||
}, [documentId, handleGlobalChange]);
|
||||
useEffect(handleGlobalChange, [documentId, handleGlobalChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (customEmoji) return undefined;
|
||||
|
||||
handlers.add(handleGlobalChange);
|
||||
|
||||
return () => {
|
||||
|
||||
@ -38,7 +38,7 @@ const CustomEmojiButton: FC<OwnProps> = ({
|
||||
onMouseDown={handleClick}
|
||||
title={emoji.emoji}
|
||||
>
|
||||
<CustomEmoji documentId={emoji.id} size={CUSTOM_EMOJI_SIZE} withPreview />
|
||||
<CustomEmoji documentId={emoji.id} size={CUSTOM_EMOJI_SIZE} shouldPreloadPreview />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,26 +1,29 @@
|
||||
import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types';
|
||||
import { getCustomEmojiPreviewMediaData } from '../../../../util/customEmojiManager';
|
||||
import placeholderSrc from '../../../../assets/square.svg';
|
||||
|
||||
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
|
||||
|
||||
export function buildCustomEmojiHtml(emoji: ApiSticker) {
|
||||
const mediaData = getCustomEmojiPreviewMediaData(emoji.id);
|
||||
const src = mediaData && `src="${mediaData}"`;
|
||||
|
||||
return `<img
|
||||
class="custom-emoji emoji emoji-small"
|
||||
draggable="false"
|
||||
alt="${emoji.emoji}"
|
||||
data-document-id="${emoji.id}"
|
||||
${src} />`;
|
||||
class="custom-emoji emoji emoji-small${!mediaData ? ' placeholder' : ''}"
|
||||
draggable="false"
|
||||
alt="${emoji.emoji}"
|
||||
data-document-id="${emoji.id}"
|
||||
src="${mediaData || placeholderSrc}"
|
||||
/>`;
|
||||
}
|
||||
|
||||
export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) {
|
||||
const mediaData = getCustomEmojiPreviewMediaData(entity.documentId);
|
||||
const src = mediaData && `src="${mediaData}"`;
|
||||
|
||||
return `<img
|
||||
class="custom-emoji emoji emoji-small"
|
||||
draggable="false"
|
||||
alt="${rawText}"
|
||||
data-document-id="${entity.documentId}"
|
||||
${src} />`;
|
||||
class="custom-emoji emoji emoji-small${!mediaData ? ' placeholder' : ''}"
|
||||
draggable="false"
|
||||
alt="${rawText}"
|
||||
data-document-id="${entity.documentId}"
|
||||
src="${mediaData || placeholderSrc}"
|
||||
/>`;
|
||||
}
|
||||
|
||||
@ -919,7 +919,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
<CustomEmoji
|
||||
documentId={senderEmojiStatus.documentId}
|
||||
loopLimit={EMOJI_STATUS_LOOP_LIMIT}
|
||||
observeIntersection={observeIntersectionForAnimatedStickers}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
|
||||
/>
|
||||
)}
|
||||
{!asForwarded && !senderEmojiStatus && senderIsPremium && <PremiumIcon />}
|
||||
|
||||
@ -5,7 +5,7 @@ import type { ApiMessage } from '../../../api/types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
|
||||
import { getMessageMediaFormat, getMessageMediaHash, getStickerPreviewHash } from '../../../global/helpers';
|
||||
import { getMessageMediaHash } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
||||
import { getActions } from '../../../global';
|
||||
@ -13,13 +13,11 @@ import { getActions } from '../../../global';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useThumbnail from '../../../hooks/useThumbnail';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import StickerView from '../../common/StickerView';
|
||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||
import OptimizedVideo from '../../ui/OptimizedVideo';
|
||||
|
||||
import './Sticker.scss';
|
||||
|
||||
@ -45,56 +43,27 @@ const Sticker: FC<OwnProps> = ({
|
||||
const { showNotification, openStickerSet } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const sticker = message.content.sticker!;
|
||||
const {
|
||||
isLottie, stickerSetInfo, isVideo, hasEffect,
|
||||
} = sticker;
|
||||
const canDisplayVideo = IS_WEBM_SUPPORTED;
|
||||
const isMemojiSticker = 'isMissing' in stickerSetInfo;
|
||||
const { stickerSetInfo, isVideo, hasEffect } = sticker;
|
||||
|
||||
const [isPlayingEffect, startPlayingEffect, stopPlayingEffect] = useFlag();
|
||||
const shouldLoad = useIsIntersecting(ref, observeIntersection);
|
||||
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 mediaHash = sticker.isPreloadedGlobally ? undefined : (
|
||||
getMessageMediaHash(message, isVideo && !IS_WEBM_SUPPORTED ? 'pictogram' : 'inline')!
|
||||
);
|
||||
|
||||
const canLoad = useIsIntersecting(ref, observeIntersection);
|
||||
const canPlay = useIsIntersecting(ref, observeIntersectionForPlaying);
|
||||
const mediaHashEffect = `sticker${sticker.id}?size=f`;
|
||||
const effectBlobUrl = useMedia(
|
||||
mediaHashEffect,
|
||||
!shouldLoad || !hasEffect,
|
||||
!canLoad || !hasEffect,
|
||||
ApiMediaFormat.BlobUrl,
|
||||
lastSyncTime,
|
||||
);
|
||||
|
||||
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 [isPlayingEffect, startPlayingEffect, stopPlayingEffect] = useFlag();
|
||||
|
||||
const handleEffectEnded = useCallback(() => {
|
||||
stopPlayingEffect();
|
||||
@ -102,11 +71,11 @@ const Sticker: FC<OwnProps> = ({
|
||||
}, [onStopEffect, stopPlayingEffect]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasEffect && shouldPlay && shouldPlayEffect) {
|
||||
if (hasEffect && canPlay && shouldPlayEffect) {
|
||||
startPlayingEffect();
|
||||
onPlayEffect?.();
|
||||
}
|
||||
}, [hasEffect, shouldPlayEffect, onPlayEffect, shouldPlay, startPlayingEffect]);
|
||||
}, [hasEffect, canPlay, onPlayEffect, shouldPlayEffect, startPlayingEffect]);
|
||||
|
||||
const openModal = useCallback(() => {
|
||||
openStickerSet({
|
||||
@ -132,50 +101,33 @@ const Sticker: FC<OwnProps> = ({
|
||||
openModal();
|
||||
}, [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 (
|
||||
<div ref={ref} className={stickerClassName} onClick={!isMemojiSticker ? handleClick : undefined}>
|
||||
{(!isMediaReady || (isVideo && !canDisplayVideo)) && (
|
||||
<img
|
||||
src={previewUrl}
|
||||
width={width}
|
||||
height={height}
|
||||
alt=""
|
||||
className={thumbClassName}
|
||||
/>
|
||||
)}
|
||||
{!isLottie && !isVideo && (
|
||||
<img
|
||||
src={mediaData as string}
|
||||
width={width}
|
||||
height={height}
|
||||
alt=""
|
||||
className={buildClassName('full-media', transitionClassNames)}
|
||||
/>
|
||||
)}
|
||||
{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 && (
|
||||
<div
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={`width: ${width}px; height: ${height}px;`}
|
||||
onClick={!isMemojiSticker ? handleClick : undefined}
|
||||
>
|
||||
<StickerView
|
||||
containerRef={ref}
|
||||
sticker={sticker}
|
||||
fullMediaHash={mediaHash}
|
||||
fullMediaClassName="full-media"
|
||||
size={width}
|
||||
shouldLoop={shouldLoop}
|
||||
noLoad={!canLoad}
|
||||
noPlay={!canPlay}
|
||||
cacheBuster={lastSyncTime}
|
||||
/>
|
||||
{hasEffect && canLoad && isPlayingEffect && (
|
||||
<AnimatedSticker
|
||||
key={mediaHashEffect}
|
||||
className="effect-sticker"
|
||||
|
||||
@ -85,8 +85,7 @@ export function updateStickerSet(
|
||||
setIds = setIds.filter((id) => id !== stickerSetId);
|
||||
}
|
||||
|
||||
const customEmojiById = isCustomEmoji && currentStickerSet.stickers
|
||||
&& buildCollectionByKey(currentStickerSet.stickers, 'id');
|
||||
const customEmojiById = isCustomEmoji && update.stickers && buildCollectionByKey(update.stickers, 'id');
|
||||
|
||||
return {
|
||||
...global,
|
||||
|
||||
@ -21,13 +21,18 @@ const updateLastRendered = throttle(() => {
|
||||
RENDER_HISTORY.clear();
|
||||
}, THROTTLE, false);
|
||||
|
||||
export default function useEnsureCustomEmoji(id: string) {
|
||||
RENDER_HISTORY.add(id);
|
||||
export function notifyCustomEmojiRender(emojiId: string) {
|
||||
RENDER_HISTORY.add(emojiId);
|
||||
updateLastRendered();
|
||||
}
|
||||
|
||||
export default function useEnsureCustomEmoji(id: string) {
|
||||
notifyCustomEmojiRender(id);
|
||||
|
||||
if (getGlobal().customEmojis.byId[id]) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOAD_QUEUE.add(id);
|
||||
loadFromQueue();
|
||||
}
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
import { ApiMediaFormat } from '../api/types';
|
||||
|
||||
import { getStickerPreviewHash } from '../global/helpers';
|
||||
import { notifyCustomEmojiRender } from '../hooks/useEnsureCustomEmoji';
|
||||
import * as mediaLoader from './mediaLoader';
|
||||
import { throttle } from './schedulers';
|
||||
|
||||
const DOM_PROCESS_THROTTLE = 500;
|
||||
|
||||
function processDomForCustomEmoji() {
|
||||
const emojis = document.querySelectorAll<HTMLImageElement>('img[data-document-id]:not([src])');
|
||||
const emojis = document.querySelectorAll<HTMLImageElement>('.custom-emoji.placeholder');
|
||||
emojis.forEach((emoji) => {
|
||||
const mediaHash = getStickerPreviewHash(emoji.dataset.documentId!);
|
||||
const emojiId = emoji.dataset.documentId!;
|
||||
const mediaHash = getStickerPreviewHash(emojiId);
|
||||
const mediaData = mediaLoader.getFromMemory(mediaHash);
|
||||
if (mediaData) {
|
||||
emoji.src = mediaData;
|
||||
emoji.classList.remove('placeholder');
|
||||
|
||||
notifyCustomEmojiRender(emojiId);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -23,6 +28,7 @@ export function getCustomEmojiPreviewMediaData(emojiId: string) {
|
||||
const mediaHash = getStickerPreviewHash(emojiId);
|
||||
const data = mediaLoader.getFromMemory(mediaHash);
|
||||
if (data) {
|
||||
notifyCustomEmojiRender(emojiId);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user