diff --git a/src/assets/square.svg b/src/assets/square.svg new file mode 100644 index 000000000..439178d10 --- /dev/null +++ b/src/assets/square.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/CustomEmoji.module.scss b/src/components/common/CustomEmoji.module.scss index a6dda78c7..2559cbf7c 100644 --- a/src/components/common/CustomEmoji.module.scss +++ b/src/components/common/CustomEmoji.module.scss @@ -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; diff --git a/src/components/common/CustomEmoji.tsx b/src/components/common/CustomEmoji.tsx index 6bbd87ea3..237c5ef8b 100644 --- a/src/components/common/CustomEmoji.tsx +++ b/src/components/common/CustomEmoji.tsx @@ -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 = ({ documentId, - children, size = STICKER_SIZE, className, loopLimit, withGridFix, - withPreview, + shouldPreloadPreview, observeIntersection, + observeIntersectionForPlaying, onClick, }) => { // eslint-disable-next-line no-null/no-null const ref = useRef(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 = ({ 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 = ({ } }, [loopLimit]); - function renderContent() { - if (!customEmoji || (!thumbDataUri && !mediaData)) { - return (children && renderText(children, ['emoji'])); - } - - if (!mediaData && !previewMediaData) { - return ( - {customEmoji.emoji} - ); - } - - if (shouldDisplayPreview || isUnsupportedVideo || (!customEmoji.isVideo && !customEmoji.isLottie)) { - return ( - {customEmoji.emoji} - ); - } - - if (customEmoji.isVideo) { - return ( - - ); - } - - return ( - - ); - } - return (
= ({ 'emoji', hasCustomColor && 'custom-color', withGridFix && styles.withGridFix, - ...transitionClassNames, )} onClick={onClick} > - {renderContent()} + {!customEmoji ? ( + Emoji + ) : ( + + )}
); }; diff --git a/src/components/common/StickerButton.tsx b/src/components/common/StickerButton.tsx index 690d43893..c000c2674 100644 --- a/src/components/common/StickerButton.tsx +++ b/src/components/common/StickerButton.tsx @@ -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 = (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 = { - 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 = { - openStickerSet({ stickerSetInfo: sticker.stickerSetInfo }); - }, [openStickerSet, sticker]); + openStickerSet({ stickerSetInfo }); + }, [openStickerSet, stickerSetInfo]); const shouldShowCloseButton = !IS_TOUCH_ENV && onRemoveRecentClick; @@ -198,15 +154,14 @@ const StickerButton = { + if (noContextMenu || isCustomEmoji) return []; + const items: ReactNode[] = []; - if (noContextMenu || isCustomEmoji) return items; if (onUnfaveClick) { items.push( @@ -261,36 +216,21 @@ const StickerButton = - {!canLottiePlay && !canVideoPlay && ( - // eslint-disable-next-line jsx-a11y/alt-text - - )} - {isVideo && ( - - )} - {shouldPlay && lottieData && ( - - )} + {isLocked && (
)} - {isPremiumSticker && !isLocked && ( + {isPremium && !isLocked && (
diff --git a/src/components/common/StickerView.module.scss b/src/components/common/StickerView.module.scss new file mode 100644 index 000000000..85ff19304 --- /dev/null +++ b/src/components/common/StickerView.module.scss @@ -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; + } +} diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx new file mode 100644 index 000000000..31b13323e --- /dev/null +++ b/src/components/common/StickerView.tsx @@ -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; + 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 = ({ + 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 ( + <> + + {isLottie ? ( + + ) : isVideo ? ( + + ) : ( + {emoji} + )} + + ); +}; + +export default memo(StickerView); diff --git a/src/components/common/helpers/renderTextWithEntities.tsx b/src/components/common/helpers/renderTextWithEntities.tsx index 736d100eb..380dd1a32 100644 --- a/src/components/common/helpers/renderTextWithEntities.tsx +++ b/src/components/common/helpers/renderTextWithEntities.tsx @@ -310,9 +310,7 @@ function processEntity( if (entity.type === ApiMessageEntityTypes.CustomEmoji) { return ( - - {renderNestedMessagePart()} - + ); } return text; @@ -420,9 +418,7 @@ function processEntity( return {renderNestedMessagePart()}; case ApiMessageEntityTypes.CustomEmoji: return ( - - {renderNestedMessagePart()} - + ); default: return renderNestedMessagePart(); diff --git a/src/components/common/hooks/useCustomEmoji.ts b/src/components/common/hooks/useCustomEmoji.ts index 9eebdd6da..971c81cba 100644 --- a/src/components/common/hooks/useCustomEmoji.ts +++ b/src/components/common/hooks/useCustomEmoji.ts @@ -10,14 +10,10 @@ const handlers = new Set(); 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 () => { diff --git a/src/components/middle/composer/CustomEmojiButton.tsx b/src/components/middle/composer/CustomEmojiButton.tsx index 6ff37568c..614e903ab 100644 --- a/src/components/middle/composer/CustomEmojiButton.tsx +++ b/src/components/middle/composer/CustomEmojiButton.tsx @@ -38,7 +38,7 @@ const CustomEmojiButton: FC = ({ onMouseDown={handleClick} title={emoji.emoji} > - + ); }; diff --git a/src/components/middle/composer/helpers/customEmoji.ts b/src/components/middle/composer/helpers/customEmoji.ts index 1da05e432..6b63ea2f8 100644 --- a/src/components/middle/composer/helpers/customEmoji.ts +++ b/src/components/middle/composer/helpers/customEmoji.ts @@ -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 `${emoji.emoji}`; + 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 `${rawText}`; + class="custom-emoji emoji emoji-small${!mediaData ? ' placeholder' : ''}" + draggable="false" + alt="${rawText}" + data-document-id="${entity.documentId}" + src="${mediaData || placeholderSrc}" + />`; } diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index b5d343867..005c85426 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -919,7 +919,8 @@ const Message: FC = ({ )} {!asForwarded && !senderEmojiStatus && senderIsPremium && } diff --git a/src/components/middle/message/Sticker.tsx b/src/components/middle/message/Sticker.tsx index 617795584..065fc2e36 100644 --- a/src/components/middle/message/Sticker.tsx +++ b/src/components/middle/message/Sticker.tsx @@ -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 = ({ const { showNotification, openStickerSet } = getActions(); const lang = useLang(); + // eslint-disable-next-line no-null/no-null const ref = useRef(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 = ({ }, [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 = ({ 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 ( -
- {(!isMediaReady || (isVideo && !canDisplayVideo)) && ( - - )} - {!isLottie && !isVideo && ( - - )} - {isVideo && canDisplayVideo && isMediaReady && ( - - )} - {isLottie && isMediaLoaded && ( - - )} - {hasEffect && shouldLoad && isPlayingEffect && ( +
+ + {hasEffect && canLoad && isPlayingEffect && ( id !== stickerSetId); } - const customEmojiById = isCustomEmoji && currentStickerSet.stickers - && buildCollectionByKey(currentStickerSet.stickers, 'id'); + const customEmojiById = isCustomEmoji && update.stickers && buildCollectionByKey(update.stickers, 'id'); return { ...global, diff --git a/src/hooks/useEnsureCustomEmoji.ts b/src/hooks/useEnsureCustomEmoji.ts index 16b30b93e..de3ffb145 100644 --- a/src/hooks/useEnsureCustomEmoji.ts +++ b/src/hooks/useEnsureCustomEmoji.ts @@ -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(); } diff --git a/src/util/customEmojiManager.ts b/src/util/customEmojiManager.ts index 2d4512669..5b962724f 100644 --- a/src/util/customEmojiManager.ts +++ b/src/util/customEmojiManager.ts @@ -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('img[data-document-id]:not([src])'); + const emojis = document.querySelectorAll('.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; }