Composer: Support animated emojis in input (#2143)
This commit is contained in:
parent
980e1e062d
commit
a7ebcaf664
@ -13,7 +13,6 @@ import buildClassName from '../../util/buildClassName';
|
||||
import safePlay from '../../util/safePlay';
|
||||
import { selectIsAlwaysHighPriorityEmoji, selectIsDefaultEmojiStatusPack } from '../../global/selectors';
|
||||
|
||||
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
|
||||
import useCustomEmoji from './hooks/useCustomEmoji';
|
||||
|
||||
import StickerView from './StickerView';
|
||||
@ -72,7 +71,6 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
|
||||
// An alternative to `withGlobal` to avoid adding numerous global containers
|
||||
const customEmoji = useCustomEmoji(documentId);
|
||||
useEnsureCustomEmoji(documentId);
|
||||
|
||||
const loopCountRef = useRef(0);
|
||||
const [shouldLoop, setShouldLoop] = useState(true);
|
||||
|
||||
@ -1,27 +1,17 @@
|
||||
import { useCallback, useEffect, useState } from '../../../lib/teact/teact';
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
import { getGlobal } from '../../../global';
|
||||
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { ApiSticker } from '../../../api/types';
|
||||
|
||||
const handlers = new Set<AnyToVoidFunction>();
|
||||
import { addCustomEmojiCallback, removeCustomEmojiCallback } from '../../../util/customEmojiManager';
|
||||
|
||||
let prevGlobal: GlobalState | undefined;
|
||||
|
||||
addCallback((global: GlobalState) => {
|
||||
if (global.customEmojis.byId !== prevGlobal?.customEmojis.byId) {
|
||||
for (const handler of handlers) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
|
||||
prevGlobal = global;
|
||||
});
|
||||
import useEnsureCustomEmoji from '../../../hooks/useEnsureCustomEmoji';
|
||||
|
||||
export default function useCustomEmoji(documentId: string) {
|
||||
const [customEmoji, setCustomEmoji] = useState<ApiSticker | undefined>(getGlobal().customEmojis.byId[documentId]);
|
||||
|
||||
useEnsureCustomEmoji(documentId);
|
||||
|
||||
const handleGlobalChange = useCallback(() => {
|
||||
setCustomEmoji(getGlobal().customEmojis.byId[documentId]);
|
||||
}, [documentId]);
|
||||
@ -31,10 +21,10 @@ export default function useCustomEmoji(documentId: string) {
|
||||
useEffect(() => {
|
||||
if (customEmoji) return undefined;
|
||||
|
||||
handlers.add(handleGlobalChange);
|
||||
addCustomEmojiCallback(handleGlobalChange, documentId);
|
||||
|
||||
return () => {
|
||||
handlers.delete(handleGlobalChange);
|
||||
removeCustomEmojiCallback(handleGlobalChange);
|
||||
};
|
||||
}, [customEmoji, documentId, handleGlobalChange]);
|
||||
|
||||
|
||||
@ -58,6 +58,7 @@ import useNativeCopySelectedMessages from '../../hooks/useNativeCopySelectedMess
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps';
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import { useResizeObserver } from '../../hooks/useResizeObserver';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
import MessageListContent from './MessageListContent';
|
||||
@ -297,26 +298,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
}, [updateStickyDates, hasTools, type, setScrollOffset, chatId, threadId]);
|
||||
|
||||
// Container resize observer (caused by Composer reply/webpage panels)
|
||||
useEffect(() => {
|
||||
if (!('ResizeObserver' in window) || process.env.APP_ENV === 'perf') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(([entry]) => {
|
||||
// During animation
|
||||
if (!(entry.target as HTMLDivElement).offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
setContainerHeight(entry.contentRect.height);
|
||||
});
|
||||
|
||||
observer.observe(containerRef.current!);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
const handleResize = useCallback((entry: ResizeObserverEntry) => {
|
||||
setContainerHeight(entry.contentRect.height);
|
||||
}, []);
|
||||
useResizeObserver(containerRef, handleResize);
|
||||
|
||||
// Memorize height for scroll animation
|
||||
const { height: windowHeight } = useWindowSize();
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
.AttachMenu {
|
||||
align-self: flex-end;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
&--button {
|
||||
&:focus {
|
||||
|
||||
@ -77,6 +77,7 @@
|
||||
|
||||
.drop-target {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
|
||||
@ -249,12 +249,21 @@
|
||||
.message-input-wrapper {
|
||||
display: flex;
|
||||
|
||||
.input-scroller {
|
||||
margin-right: 0.5rem;
|
||||
padding-right: 2.5rem;
|
||||
}
|
||||
|
||||
> .Spinner {
|
||||
align-self: center;
|
||||
--spinner-size: 1.5rem;
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
|
||||
.inline-bot-spinner {
|
||||
margin-right: 3rem;
|
||||
}
|
||||
|
||||
> .AttachMenu > .Button,
|
||||
> .Button {
|
||||
flex-shrink: 0;
|
||||
@ -402,6 +411,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
.input-scroller {
|
||||
min-height: 3.5rem;
|
||||
max-height: 26rem;
|
||||
overflow: hidden;
|
||||
|
||||
&.overflown {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
min-height: 2.875rem;
|
||||
max-height: 16rem;
|
||||
}
|
||||
|
||||
& > .input-scroller-content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition: height 100ms ease;
|
||||
|
||||
body.animation-level-0 & {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin: 0 1px -5px;
|
||||
vertical-align: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.custom-emoji {
|
||||
margin: 0;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
}
|
||||
|
||||
#message-input-text,
|
||||
#caption-input-text {
|
||||
position: relative;
|
||||
@ -410,16 +458,18 @@
|
||||
padding: calc((3.25rem - var(--composer-text-size, 1rem) * 1.375) / 2 - var(--border-width, 0) * 2)
|
||||
calc(0.9rem - var(--border-width));
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
line-height: 1.375;
|
||||
font-family: var(--font-family);
|
||||
unicode-bidi: plaintext;
|
||||
text-align: initial;
|
||||
font-size: var(--composer-text-size, 1rem);
|
||||
|
||||
&.overflown {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
caret-color: var(--color-text);
|
||||
|
||||
&.touched,
|
||||
&:focus {
|
||||
@ -496,6 +546,7 @@
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
z-index: -10;
|
||||
@ -509,30 +560,13 @@
|
||||
|
||||
.form-control {
|
||||
margin-bottom: 0;
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
caret-color: var(--color-text);
|
||||
min-height: 3.5rem;
|
||||
max-height: 26rem;
|
||||
line-height: 1.3125;
|
||||
padding: calc((3.5rem - var(--composer-text-size, 1rem) * 1.3125) / 2) 0;
|
||||
white-space: pre-wrap;
|
||||
height: auto;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
height: 2.875rem;
|
||||
min-height: 2.875rem;
|
||||
max-height: 16rem;
|
||||
padding: calc((2.875rem - var(--composer-text-size, 1rem) * 1.3125) / 2) 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition: height 100ms ease;
|
||||
|
||||
body.animation-level-0 & {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -562,25 +596,22 @@
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin: 0 1px -5px;
|
||||
vertical-align: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.custom-emoji {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
}
|
||||
|
||||
#caption-input-text {
|
||||
.form-control {
|
||||
height: 3.25rem;
|
||||
border: 1px solid var(--color-borders-input);
|
||||
border-radius: var(--border-radius-default);
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.input-scroller {
|
||||
min-height: 3.25rem;
|
||||
max-height: 15rem;
|
||||
|
||||
margin-right: 0.5rem;
|
||||
|
||||
&:has(.form-control:focus) {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
|
||||
@ -1261,7 +1261,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
onSuppressedFocus={closeSymbolMenu}
|
||||
/>
|
||||
{isInlineBotLoading && Boolean(inlineBotId) && (
|
||||
<Spinner color="gray" />
|
||||
<Spinner color="gray" className="inline-bot-spinner" />
|
||||
)}
|
||||
{withScheduledButton && (
|
||||
<Button
|
||||
|
||||
@ -17,13 +17,15 @@ import {
|
||||
} from '../../../util/environment';
|
||||
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
|
||||
import { getIsDirectTextInputDisabled } from '../../../util/directInputManager';
|
||||
import parseEmojiOnlyString from '../../../util/parseEmojiOnlyString';
|
||||
import { isSelectionInsideInput } from './helpers/selection';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevDeps';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import parseEmojiOnlyString from '../../../util/parseEmojiOnlyString';
|
||||
import { isSelectionInsideInput } from './helpers/selection';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import useInputCustomEmojis from './hooks/useInputCustomEmojis';
|
||||
|
||||
import TextFormatter from './TextFormatter';
|
||||
|
||||
@ -32,6 +34,8 @@ const CONTEXT_MENU_CLOSE_DELAY_MS = 100;
|
||||
const FOCUS_DELAY_MS = 350;
|
||||
const TRANSITION_DURATION_FACTOR = 50;
|
||||
|
||||
const SCROLLER_CLASS = 'input-scroller';
|
||||
|
||||
type OwnProps = {
|
||||
id: string;
|
||||
chatId: string;
|
||||
@ -58,6 +62,7 @@ type StateProps = {
|
||||
};
|
||||
|
||||
const MAX_INPUT_HEIGHT = IS_SINGLE_COLUMN_LAYOUT ? 256 : 416;
|
||||
const MAX_ATTACHMENT_MODAL_INPUT_HEIGHT = 240;
|
||||
const TAB_INDEX_PRIORITY_TIMEOUT = 2000;
|
||||
// Heuristics allowing the user to make a triple click
|
||||
const SELECTION_RECALCULATE_DELAY_MS = 260;
|
||||
@ -112,6 +117,14 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
const selectionTimeoutRef = useRef<number>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const cloneRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const scrollerCloneRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const absoluteContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const lang = useLang();
|
||||
const isContextMenuOpenRef = useRef(false);
|
||||
@ -120,10 +133,40 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
const [selectedRange, setSelectedRange] = useState<Range>();
|
||||
const [isTextFormatterDisabled, setIsTextFormatterDisabled] = useState<boolean>(false);
|
||||
|
||||
useInputCustomEmojis(html, inputRef, sharedCanvasRef, sharedCanvasHqRef, absoluteContainerRef);
|
||||
|
||||
const updateInputHeight = useCallback((willSend = false) => {
|
||||
const scroller = inputRef.current!.closest<HTMLDivElement>(`.${SCROLLER_CLASS}`)!;
|
||||
const clone = scrollerCloneRef.current!;
|
||||
const currentHeight = Number(scroller.style.height.replace('px', ''));
|
||||
const maxHeight = isAttachmentModalInput ? MAX_ATTACHMENT_MODAL_INPUT_HEIGHT : MAX_INPUT_HEIGHT;
|
||||
const newHeight = Math.min(clone.scrollHeight, maxHeight);
|
||||
if (newHeight === currentHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transitionDuration = Math.round(
|
||||
TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)),
|
||||
);
|
||||
|
||||
const exec = () => {
|
||||
scroller.style.height = `${newHeight}px`;
|
||||
scroller.style.transitionDuration = `${transitionDuration}ms`;
|
||||
scroller.classList.toggle('overflown', clone.scrollHeight > maxHeight);
|
||||
};
|
||||
|
||||
if (willSend) {
|
||||
// Sync with sending animation
|
||||
requestAnimationFrame(exec);
|
||||
} else {
|
||||
exec();
|
||||
}
|
||||
}, [isAttachmentModalInput]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAttachmentModalInput) return;
|
||||
updateInputHeight(false);
|
||||
}, [isAttachmentModalInput]);
|
||||
}, [isAttachmentModalInput, updateInputHeight]);
|
||||
|
||||
useLayoutEffectWithPrevDeps(([prevHtml]) => {
|
||||
if (html !== inputRef.current!.innerHTML) {
|
||||
@ -331,33 +374,6 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
function updateInputHeight(willSend = false) {
|
||||
const input = inputRef.current!;
|
||||
const clone = cloneRef.current!;
|
||||
const currentHeight = Number(input.style.height.replace('px', ''));
|
||||
const newHeight = Math.min(clone.scrollHeight, MAX_INPUT_HEIGHT);
|
||||
if (newHeight === currentHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transitionDuration = Math.round(
|
||||
TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)),
|
||||
);
|
||||
|
||||
const exec = () => {
|
||||
input.style.height = `${newHeight}px`;
|
||||
input.style.transitionDuration = `${transitionDuration}ms`;
|
||||
input.classList.toggle('overflown', clone.scrollHeight > MAX_INPUT_HEIGHT);
|
||||
};
|
||||
|
||||
if (willSend) {
|
||||
// Sync with sending animation
|
||||
requestAnimationFrame(exec);
|
||||
} else {
|
||||
exec();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_TOUCH_ENV) {
|
||||
return;
|
||||
@ -450,37 +466,47 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
}, [shouldSuppressFocus]);
|
||||
|
||||
const className = buildClassName(
|
||||
'form-control custom-scroll',
|
||||
'form-control',
|
||||
html.length > 0 && 'touched',
|
||||
shouldSuppressFocus && 'focus-disabled',
|
||||
);
|
||||
|
||||
return (
|
||||
<div id={id} onClick={shouldSuppressFocus ? onSuppressedFocus : undefined} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div
|
||||
ref={inputRef}
|
||||
id={editableInputId || EDITABLE_INPUT_ID}
|
||||
className={className}
|
||||
contentEditable
|
||||
role="textbox"
|
||||
dir="auto"
|
||||
tabIndex={0}
|
||||
onClick={focusInput}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={IS_ANDROID ? handleAndroidContextMenu : undefined}
|
||||
onTouchCancel={IS_ANDROID ? processSelectionWithTimeout : undefined}
|
||||
aria-label={placeholder}
|
||||
/>
|
||||
<div className={buildClassName('custom-scroll', SCROLLER_CLASS)}>
|
||||
<div className="input-scroller-content">
|
||||
<div
|
||||
ref={inputRef}
|
||||
id={editableInputId || EDITABLE_INPUT_ID}
|
||||
className={className}
|
||||
contentEditable
|
||||
role="textbox"
|
||||
dir="auto"
|
||||
tabIndex={0}
|
||||
onClick={focusInput}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={IS_ANDROID ? handleAndroidContextMenu : undefined}
|
||||
onTouchCancel={IS_ANDROID ? processSelectionWithTimeout : undefined}
|
||||
aria-label={placeholder}
|
||||
/>
|
||||
{!forcedPlaceholder && <span className="placeholder-text" dir="auto">{placeholder}</span>}
|
||||
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||
<canvas ref={sharedCanvasHqRef} className="shared-canvas" />
|
||||
<div ref={absoluteContainerRef} className="absolute-video-container" />
|
||||
</div>
|
||||
</div>
|
||||
<div ref={scrollerCloneRef} className={buildClassName('custom-scroll', SCROLLER_CLASS, 'clone')}>
|
||||
<div className="input-scroller-content">
|
||||
<div ref={cloneRef} className={buildClassName(className, 'clone')} dir="auto" />
|
||||
</div>
|
||||
</div>
|
||||
{captionLimit && (
|
||||
<div className="max-length-indicator" dir="auto">
|
||||
{captionLimit}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={cloneRef} className={buildClassName(className, 'clone')} dir="auto" />
|
||||
{!forcedPlaceholder && <span className="placeholder-text" dir="auto">{placeholder}</span>}
|
||||
<TextFormatter
|
||||
isOpen={isTextFormatterOpen}
|
||||
anchorPosition={textFormatterAnchorPosition}
|
||||
|
||||
@ -10,6 +10,8 @@ import { selectIsAlwaysHighPriorityEmoji } from '../../../global/selectors';
|
||||
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
||||
import { getFirstLetters } from '../../../util/textFormat';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getStickerPreviewHash } from '../../../global/helpers';
|
||||
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
||||
@ -42,8 +44,13 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
|
||||
const isIntersecting = useIsIntersecting(containerRef, observeIntersection);
|
||||
|
||||
const mediaData = useMedia((hasThumbnail || isLottie) && `stickerSet${stickerSet.id}`, !isIntersecting);
|
||||
const isReady = mediaData && (!isVideo || IS_WEBM_SUPPORTED);
|
||||
const shouldFallbackToStatic = stickerSet.stickers && isVideo && !IS_WEBM_SUPPORTED;
|
||||
const staticHash = shouldFallbackToStatic && getStickerPreviewHash(stickerSet.stickers![0].id);
|
||||
const staticMediaData = useMedia(staticHash, !isIntersecting);
|
||||
|
||||
const mediaHash = ((hasThumbnail && !shouldFallbackToStatic) || isLottie) && `stickerSet${stickerSet.id}`;
|
||||
const mediaData = useMedia(mediaHash, !isIntersecting);
|
||||
const isReady = mediaData || staticMediaData;
|
||||
const transitionClassNames = useMediaTransition(isReady);
|
||||
|
||||
const bounds = useBoundsInSharedCanvas(containerRef, sharedCanvasRef);
|
||||
@ -61,7 +68,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
sharedCanvas={sharedCanvasRef?.current || undefined}
|
||||
sharedCanvasCoords={bounds.coords}
|
||||
/>
|
||||
) : isVideo ? (
|
||||
) : (isVideo && !shouldFallbackToStatic) ? (
|
||||
<OptimizedVideo
|
||||
className={buildClassName(styles.video, transitionClassNames)}
|
||||
src={mediaData}
|
||||
@ -71,7 +78,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={mediaData}
|
||||
src={mediaData || staticMediaData}
|
||||
className={transitionClassNames}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
@ -82,7 +82,7 @@ const SymbolMenuFooter: FC<OwnProps> = ({
|
||||
{renderTabButton(SymbolMenuTabs.Stickers)}
|
||||
{renderTabButton(SymbolMenuTabs.GIFs)}
|
||||
|
||||
{activeTab === SymbolMenuTabs.Emoji && (
|
||||
{(activeTab === SymbolMenuTabs.Emoji || activeTab === SymbolMenuTabs.CustomEmoji) && (
|
||||
<Button
|
||||
className="symbol-delete-button"
|
||||
onClick={onRemoveSymbol}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
.TextFormatter {
|
||||
transform: translate(-50%, -3.25rem);
|
||||
z-index: 1;
|
||||
|
||||
&,
|
||||
&-link-control {
|
||||
|
||||
@ -1,37 +1,38 @@
|
||||
import type { ApiMessageEntityCustomEmoji, ApiSticker } from '../../../../api/types';
|
||||
import { ApiMessageEntityTypes } from '../../../../api/types';
|
||||
|
||||
import { getGlobal } from '../../../../global';
|
||||
import { EMOJI_SIZES } from '../../../../config';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
import { getCustomEmojiPreviewMediaData } from '../../../../util/customEmojiManager';
|
||||
|
||||
import placeholderSrc from '../../../../assets/square.svg';
|
||||
import { getInputCustomEmojiParams } from '../../../../util/customEmojiManager';
|
||||
|
||||
export const INPUT_CUSTOM_EMOJI_SELECTOR = 'img[data-document-id]';
|
||||
|
||||
export function buildCustomEmojiHtml(emoji: ApiSticker) {
|
||||
const mediaData = getCustomEmojiPreviewMediaData(emoji.id);
|
||||
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(emoji);
|
||||
|
||||
return `<img
|
||||
class="custom-emoji emoji emoji-small${!mediaData ? ' placeholder' : ''}"
|
||||
class="custom-emoji emoji emoji-small ${isPlaceholder ? 'placeholder' : ''}"
|
||||
draggable="false"
|
||||
alt="${emoji.emoji}"
|
||||
data-document-id="${emoji.id}"
|
||||
${uniqueId ? `data-unique-id="${uniqueId}"` : ''}
|
||||
data-entity-type="${ApiMessageEntityTypes.CustomEmoji}"
|
||||
src="${mediaData || placeholderSrc}"
|
||||
src="${src}"
|
||||
/>`;
|
||||
}
|
||||
|
||||
export function buildCustomEmojiHtmlFromEntity(rawText: string, entity: ApiMessageEntityCustomEmoji) {
|
||||
const mediaData = getCustomEmojiPreviewMediaData(entity.documentId);
|
||||
|
||||
const customEmoji = getGlobal().customEmojis.byId[entity.documentId];
|
||||
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji);
|
||||
return `<img
|
||||
class="custom-emoji emoji emoji-small${!mediaData ? ' placeholder' : ''}"
|
||||
class="custom-emoji emoji emoji-small ${isPlaceholder ? 'placeholder' : ''}"
|
||||
draggable="false"
|
||||
alt="${rawText}"
|
||||
data-document-id="${entity.documentId}"
|
||||
${uniqueId ? `data-unique-id="${uniqueId}"` : ''}
|
||||
data-entity-type="${ApiMessageEntityTypes.CustomEmoji}"
|
||||
src="${mediaData || placeholderSrc}"
|
||||
src="${src}"
|
||||
/>`;
|
||||
}
|
||||
|
||||
|
||||
@ -35,8 +35,8 @@ const useEditing = (
|
||||
if (prevEditedMessage?.id === editedMessage.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = getTextWithEntitiesAsHtml(editingDraft?.text.length ? editingDraft : editedMessage.content.text);
|
||||
const text = !prevEditedMessage && editingDraft?.text.length ? editingDraft : editedMessage.content.text;
|
||||
const html = getTextWithEntitiesAsHtml(text);
|
||||
setHtml(html);
|
||||
// `fastRaf` would execute syncronously in this case
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
198
src/components/middle/composer/hooks/useInputCustomEmojis.ts
Normal file
198
src/components/middle/composer/hooks/useInputCustomEmojis.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import {
|
||||
useCallback, useEffect, useRef,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import RLottie from '../../../../lib/rlottie/RLottie';
|
||||
|
||||
import type { ApiSticker } from '../../../../api/types';
|
||||
|
||||
import { getGlobal } from '../../../../global';
|
||||
import { selectIsAlwaysHighPriorityEmoji } from '../../../../global/selectors';
|
||||
import generateIdFor from '../../../../util/generateIdFor';
|
||||
import {
|
||||
addCustomEmojiInputRenderCallback,
|
||||
getCustomEmojiMediaDataForInput,
|
||||
removeCustomEmojiInputRenderCallback,
|
||||
} from '../../../../util/customEmojiManager';
|
||||
import { round } from '../../../../util/math';
|
||||
import { fastRaf } from '../../../../util/schedulers';
|
||||
import AbsoluteVideo from '../../../../util/AbsoluteVideo';
|
||||
|
||||
import { useResizeObserver } from '../../../../hooks/useResizeObserver';
|
||||
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
||||
|
||||
const ID_STORE = {};
|
||||
const SIZE = 20;
|
||||
|
||||
type Player = {
|
||||
play: () => void;
|
||||
pause: () => void;
|
||||
destroy: () => void;
|
||||
updatePosition: (x: number, y: number) => void;
|
||||
};
|
||||
|
||||
export default function useInputCustomEmojis(
|
||||
html: string,
|
||||
inputRef: React.RefObject<HTMLDivElement>,
|
||||
sharedCanvasRef: React.RefObject<HTMLCanvasElement>,
|
||||
sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>,
|
||||
absoluteContainerRef: React.RefObject<HTMLElement>,
|
||||
) {
|
||||
const mapRef = useRef<Map<string, Player>>(new Map());
|
||||
|
||||
const removeContainers = useCallback((ids: string[]) => {
|
||||
ids.forEach((id) => {
|
||||
const player = mapRef.current.get(id);
|
||||
if (player) {
|
||||
player.destroy();
|
||||
mapRef.current.delete(id);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const synchronizeElements = useCallback(() => {
|
||||
if (!inputRef.current || !sharedCanvasRef.current || !sharedCanvasHqRef.current) return;
|
||||
const global = getGlobal();
|
||||
const removedContainers = new Set(mapRef.current.keys());
|
||||
const customEmojies = Array.from(inputRef.current.querySelectorAll<HTMLElement>('.custom-emoji'));
|
||||
|
||||
customEmojies.forEach((element) => {
|
||||
const id = element.dataset.uniqueId!;
|
||||
const documentId = element.dataset.documentId!;
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
removedContainers.delete(id);
|
||||
|
||||
const mediaUrl = getCustomEmojiMediaDataForInput(documentId);
|
||||
if (!mediaUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvasBounds = sharedCanvasRef.current!.getBoundingClientRect();
|
||||
const elementBounds = element.getBoundingClientRect();
|
||||
const x = round((elementBounds.left - canvasBounds.left) / canvasBounds.width, 4);
|
||||
const y = round((elementBounds.top - canvasBounds.top) / canvasBounds.height, 4);
|
||||
|
||||
if (mapRef.current.has(id)) {
|
||||
const player = mapRef.current.get(id)!;
|
||||
player.updatePosition(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
const customEmoji = global.customEmojis.byId[documentId];
|
||||
if (!customEmoji) {
|
||||
return;
|
||||
}
|
||||
const isHq = customEmoji?.stickerSetInfo && selectIsAlwaysHighPriorityEmoji(global, customEmoji.stickerSetInfo);
|
||||
|
||||
const animation = createPlayer({
|
||||
customEmoji,
|
||||
sharedCanvasRef,
|
||||
sharedCanvasHqRef,
|
||||
absoluteContainerRef,
|
||||
uniqueId: id,
|
||||
mediaUrl,
|
||||
isHq,
|
||||
position: { x, y },
|
||||
});
|
||||
animation.play();
|
||||
|
||||
mapRef.current.set(id, animation);
|
||||
});
|
||||
|
||||
removeContainers(Array.from(removedContainers));
|
||||
}, [absoluteContainerRef, inputRef, removeContainers, sharedCanvasHqRef, sharedCanvasRef]);
|
||||
|
||||
useEffect(() => {
|
||||
addCustomEmojiInputRenderCallback(synchronizeElements);
|
||||
|
||||
return () => {
|
||||
removeCustomEmojiInputRenderCallback(synchronizeElements);
|
||||
};
|
||||
}, [synchronizeElements]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!html || !inputRef.current || !sharedCanvasRef.current) {
|
||||
removeContainers(Array.from(mapRef.current.keys()));
|
||||
return;
|
||||
}
|
||||
|
||||
synchronizeElements();
|
||||
}, [html, inputRef, removeContainers, sharedCanvasRef, synchronizeElements]);
|
||||
|
||||
useResizeObserver(sharedCanvasRef, synchronizeElements, true);
|
||||
|
||||
const freezeAnimation = useCallback(() => {
|
||||
mapRef.current.forEach((player) => {
|
||||
player.pause();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const unfreezeAnimation = useCallback(() => {
|
||||
mapRef.current.forEach((player) => {
|
||||
player.play();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const unfreezeAnimationOnRaf = useCallback(() => {
|
||||
fastRaf(unfreezeAnimation);
|
||||
}, [unfreezeAnimation]);
|
||||
|
||||
// Pausing frame may not happen in background,
|
||||
// so we need to make sure it happens right after focusing,
|
||||
// then we can play again.
|
||||
useBackgroundMode(freezeAnimation, unfreezeAnimationOnRaf);
|
||||
}
|
||||
|
||||
function createPlayer({
|
||||
customEmoji,
|
||||
sharedCanvasRef,
|
||||
sharedCanvasHqRef,
|
||||
absoluteContainerRef,
|
||||
uniqueId,
|
||||
mediaUrl,
|
||||
position,
|
||||
isHq,
|
||||
} : {
|
||||
customEmoji: ApiSticker;
|
||||
sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
|
||||
sharedCanvasHqRef: React.RefObject<HTMLCanvasElement>;
|
||||
absoluteContainerRef: React.RefObject<HTMLElement>;
|
||||
uniqueId: string;
|
||||
mediaUrl: string;
|
||||
position: { x: number; y: number };
|
||||
isHq?: boolean;
|
||||
}): Player {
|
||||
if (customEmoji.isLottie) {
|
||||
const lottie = RLottie.init(
|
||||
uniqueId,
|
||||
isHq ? sharedCanvasHqRef.current! : sharedCanvasRef.current!,
|
||||
undefined,
|
||||
generateIdFor(ID_STORE, true),
|
||||
mediaUrl,
|
||||
{
|
||||
size: SIZE,
|
||||
coords: position,
|
||||
isLowPriority: !isHq,
|
||||
},
|
||||
);
|
||||
return {
|
||||
play: () => lottie.play(),
|
||||
pause: () => lottie.pause(),
|
||||
destroy: () => lottie.removeContainer(uniqueId),
|
||||
updatePosition: (x: number, y: number) => lottie.setSharedCanvasCoords(uniqueId, { x, y }),
|
||||
};
|
||||
}
|
||||
|
||||
if (customEmoji.isVideo) {
|
||||
const absoluteVideo = new AbsoluteVideo(mediaUrl, absoluteContainerRef.current!, { size: SIZE, position });
|
||||
return {
|
||||
play: () => absoluteVideo.play(),
|
||||
pause: () => absoluteVideo.pause(),
|
||||
destroy: () => absoluteVideo.destroy(),
|
||||
updatePosition: (x: number, y: number) => absoluteVideo.updatePosition({ x, y }),
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('Unsupported custom emoji type');
|
||||
}
|
||||
@ -8,12 +8,17 @@ import './Spinner.scss';
|
||||
const Spinner: FC<{
|
||||
color?: 'blue' | 'white' | 'black' | 'green' | 'gray' | 'yellow';
|
||||
backgroundColor?: 'light' | 'dark';
|
||||
className?: string;
|
||||
}> = ({
|
||||
color = 'blue',
|
||||
backgroundColor,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className={buildClassName('Spinner', color, backgroundColor && 'with-background', `bg-${backgroundColor}`)}>
|
||||
<div className={buildClassName(
|
||||
'Spinner', className, color, backgroundColor && 'with-background', `bg-${backgroundColor}`,
|
||||
)}
|
||||
>
|
||||
<div />
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import {
|
||||
useCallback, useEffect, useLayoutEffect, useMemo, useState,
|
||||
useCallback, useLayoutEffect, useMemo, useState,
|
||||
} from '../lib/teact/teact';
|
||||
|
||||
import { throttle } from '../util/schedulers';
|
||||
import { round } from '../util/math';
|
||||
|
||||
import { useResizeObserver } from './useResizeObserver';
|
||||
|
||||
export default function useBoundsInSharedCanvas(
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>,
|
||||
@ -32,26 +33,7 @@ export default function useBoundsInSharedCanvas(
|
||||
|
||||
useLayoutEffect(recalculate, [recalculate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!('ResizeObserver' in window) || !sharedCanvasRef?.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(throttle(([entry]) => {
|
||||
// During animation
|
||||
if (!(entry.target as HTMLCanvasElement).offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
recalculate();
|
||||
}, 300, false));
|
||||
|
||||
observer.observe(sharedCanvasRef.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [recalculate, sharedCanvasRef]);
|
||||
useResizeObserver(sharedCanvasRef, recalculate, true);
|
||||
|
||||
const coords = useMemo(() => (x !== undefined && y !== undefined ? { x, y } : undefined), [x, y]);
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { getActions, getGlobal } from '../global';
|
||||
import { addCustomEmojiInputRenderCallback } from '../util/customEmojiManager';
|
||||
|
||||
import { throttle } from '../util/schedulers';
|
||||
|
||||
@ -24,11 +25,13 @@ const updateLastRendered = throttle(() => {
|
||||
RENDER_HISTORY.clear();
|
||||
}, THROTTLE, false);
|
||||
|
||||
export function notifyCustomEmojiRender(emojiId: string) {
|
||||
function notifyCustomEmojiRender(emojiId: string) {
|
||||
RENDER_HISTORY.add(emojiId);
|
||||
updateLastRendered();
|
||||
}
|
||||
|
||||
addCustomEmojiInputRenderCallback(notifyCustomEmojiRender);
|
||||
|
||||
export default function useEnsureCustomEmoji(id: string) {
|
||||
const lastSyncTime = useLastSyncTime();
|
||||
notifyCustomEmojiRender(id);
|
||||
|
||||
32
src/hooks/useResizeObserver.ts
Normal file
32
src/hooks/useResizeObserver.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { useEffect } from '../lib/teact/teact';
|
||||
import { throttle } from '../util/schedulers';
|
||||
|
||||
const THROTTLE = 300;
|
||||
|
||||
export function useResizeObserver(
|
||||
ref: React.RefObject<HTMLElement> | undefined,
|
||||
onResize: (entry: ResizeObserverEntry) => void,
|
||||
withThrottle = false,
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!('ResizeObserver' in window) || !ref?.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const callback: ResizeObserverCallback = ([entry]) => {
|
||||
// During animation
|
||||
if (!(entry.target as HTMLElement).offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
onResize(entry);
|
||||
};
|
||||
const observer = new ResizeObserver(withThrottle ? throttle(callback, THROTTLE, false) : callback);
|
||||
|
||||
observer.observe(ref.current);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [onResize, ref, withThrottle]);
|
||||
}
|
||||
@ -206,7 +206,7 @@ class RLottie {
|
||||
setSharedCanvasCoords(containerId: string, newCoords: Params['coords']) {
|
||||
const containerInfo = this.containers.get(containerId)!;
|
||||
const {
|
||||
canvas, ctx, isPaused, coords,
|
||||
canvas, ctx,
|
||||
} = containerInfo;
|
||||
|
||||
if (!canvas.dataset.isJustCleaned || canvas.dataset.isJustCleaned === 'false') {
|
||||
@ -224,12 +224,10 @@ class RLottie {
|
||||
y: Math.round((newCoords?.y || 0) * canvas.height),
|
||||
};
|
||||
|
||||
if (isPaused || !this.isPlaying()) {
|
||||
const frame = this.getFrame(this.prevFrameIndex) || this.getFrame(Math.round(this.approxFrameIndex));
|
||||
const frame = this.getFrame(this.prevFrameIndex) || this.getFrame(Math.round(this.approxFrameIndex));
|
||||
|
||||
if (frame && frame !== WAITING) {
|
||||
ctx.drawImage(frame, coords!.x, coords!.y);
|
||||
}
|
||||
if (frame && frame !== WAITING) {
|
||||
ctx.drawImage(frame, containerInfo.coords.x, containerInfo.coords.y);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -290,7 +290,7 @@ div[role="button"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.shared-canvas {
|
||||
.shared-canvas, .absolute-video-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
71
src/util/AbsoluteVideo.ts
Normal file
71
src/util/AbsoluteVideo.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import safePlay from './safePlay';
|
||||
|
||||
type AbsoluteVideoOptions = {
|
||||
position: { x: number; y: number };
|
||||
noLoop?: boolean;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export default class AbsoluteVideo {
|
||||
private video?: HTMLVideoElement;
|
||||
|
||||
private isPlaying = false;
|
||||
|
||||
constructor(
|
||||
videoUrl: string,
|
||||
private container: HTMLElement,
|
||||
private options: AbsoluteVideoOptions,
|
||||
) {
|
||||
this.video = document.createElement('video');
|
||||
this.video.src = videoUrl;
|
||||
this.video.disablePictureInPicture = true;
|
||||
this.video.muted = true;
|
||||
this.video.style.position = 'absolute';
|
||||
this.video.load();
|
||||
|
||||
if (!this.options.noLoop) {
|
||||
this.video.loop = true;
|
||||
}
|
||||
|
||||
this.container.appendChild(this.video);
|
||||
this.recalculatePositionAndSize();
|
||||
}
|
||||
|
||||
public play() {
|
||||
if (this.isPlaying || !this.video) return;
|
||||
this.recalculatePositionAndSize();
|
||||
if (this.video.paused) {
|
||||
safePlay(this.video);
|
||||
}
|
||||
this.isPlaying = true;
|
||||
}
|
||||
|
||||
public pause() {
|
||||
if (!this.isPlaying || !this.video) return;
|
||||
if (!this.video.paused) {
|
||||
this.video.pause();
|
||||
}
|
||||
this.isPlaying = false;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.pause();
|
||||
this.video?.remove();
|
||||
this.video = undefined;
|
||||
}
|
||||
|
||||
public updatePosition(position: AbsoluteVideoOptions['position']) {
|
||||
this.options.position = position;
|
||||
this.recalculatePositionAndSize();
|
||||
}
|
||||
|
||||
private recalculatePositionAndSize() {
|
||||
if (!this.video) return;
|
||||
const { size, position: { x, y } } = this.options;
|
||||
const { width, height } = this.container.getBoundingClientRect();
|
||||
this.video.style.left = `${Math.round(x * width)}px`;
|
||||
this.video.style.top = `${Math.round(y * height)}px`;
|
||||
this.video.style.width = `${size}px`;
|
||||
this.video.style.height = `${size}px`;
|
||||
}
|
||||
}
|
||||
@ -1,37 +1,124 @@
|
||||
import { addCallback } from '../lib/teact/teactn';
|
||||
import { getGlobal } from '../global';
|
||||
|
||||
import { ApiMediaFormat } from '../api/types';
|
||||
import type { ApiSticker } from '../api/types';
|
||||
import type { GlobalState } from '../global/types';
|
||||
|
||||
import { getStickerPreviewHash } from '../global/helpers';
|
||||
import { notifyCustomEmojiRender } from '../hooks/useEnsureCustomEmoji';
|
||||
import * as mediaLoader from './mediaLoader';
|
||||
import { throttle } from './schedulers';
|
||||
import generateIdFor from './generateIdFor';
|
||||
import { IS_WEBM_SUPPORTED } from './environment';
|
||||
|
||||
import placeholderSrc from '../assets/square.svg';
|
||||
import blankSrc from '../assets/blank.png';
|
||||
|
||||
type CustomEmojiLoadCallback = (customEmojis: GlobalState['customEmojis']) => void;
|
||||
type CustomEmojiInputRenderCallback = (emojiId: string) => void;
|
||||
|
||||
const ID_STORE = {};
|
||||
const DOM_PROCESS_THROTTLE = 500;
|
||||
|
||||
const INPUT_WAITING_CUSTOM_EMOJI_IDS: Set<string> = new Set();
|
||||
|
||||
const handlers = new Map<CustomEmojiLoadCallback, string>();
|
||||
const renderHandlers = new Set<CustomEmojiInputRenderCallback>();
|
||||
|
||||
let prevGlobal: GlobalState | undefined;
|
||||
|
||||
addCallback((global: GlobalState) => {
|
||||
if (global.customEmojis.byId !== prevGlobal?.customEmojis.byId) {
|
||||
for (const entry of handlers) {
|
||||
const [handler, id] = entry;
|
||||
if (global.customEmojis.byId[id]) {
|
||||
handler(global.customEmojis);
|
||||
}
|
||||
}
|
||||
|
||||
checkInputCustomEmojiLoad(global.customEmojis);
|
||||
}
|
||||
|
||||
prevGlobal = global;
|
||||
});
|
||||
|
||||
export function addCustomEmojiCallback(handler: CustomEmojiLoadCallback, emojiId: string) {
|
||||
handlers.set(handler, emojiId);
|
||||
}
|
||||
|
||||
export function removeCustomEmojiCallback(handler: CustomEmojiLoadCallback) {
|
||||
handlers.delete(handler);
|
||||
}
|
||||
|
||||
export function addCustomEmojiInputRenderCallback(handler: AnyToVoidFunction) {
|
||||
renderHandlers.add(handler);
|
||||
}
|
||||
|
||||
export function removeCustomEmojiInputRenderCallback(handler: AnyToVoidFunction) {
|
||||
renderHandlers.delete(handler);
|
||||
}
|
||||
|
||||
const callInputRenderHandlers = throttle((emojiId: string) => {
|
||||
renderHandlers.forEach((handler) => handler(emojiId));
|
||||
}, DOM_PROCESS_THROTTLE);
|
||||
|
||||
function processDomForCustomEmoji() {
|
||||
const emojis = document.querySelectorAll<HTMLImageElement>('.custom-emoji.placeholder');
|
||||
emojis.forEach((emoji) => {
|
||||
const emojiId = emoji.dataset.documentId!;
|
||||
const mediaHash = getStickerPreviewHash(emojiId);
|
||||
const mediaData = mediaLoader.getFromMemory(mediaHash);
|
||||
if (mediaData) {
|
||||
emoji.src = mediaData;
|
||||
emoji.classList.remove('placeholder');
|
||||
const customEmoji = getGlobal().customEmojis.byId[emoji.dataset.documentId!];
|
||||
if (!customEmoji) {
|
||||
INPUT_WAITING_CUSTOM_EMOJI_IDS.add(emoji.dataset.documentId!);
|
||||
return;
|
||||
}
|
||||
const [isPlaceholder, src, uniqueId] = getInputCustomEmojiParams(customEmoji);
|
||||
|
||||
notifyCustomEmojiRender(emojiId);
|
||||
if (!isPlaceholder) {
|
||||
emoji.src = src;
|
||||
emoji.classList.remove('placeholder');
|
||||
if (uniqueId) emoji.dataset.uniqueId = uniqueId;
|
||||
|
||||
callInputRenderHandlers(customEmoji.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const processMessageInputForCustomEmoji = throttle(processDomForCustomEmoji, DOM_PROCESS_THROTTLE);
|
||||
|
||||
export function getCustomEmojiPreviewMediaData(emojiId: string) {
|
||||
const mediaHash = getStickerPreviewHash(emojiId);
|
||||
function checkInputCustomEmojiLoad(customEmojis: GlobalState['customEmojis']) {
|
||||
const loaded = Array.from(INPUT_WAITING_CUSTOM_EMOJI_IDS).filter((id) => Boolean(customEmojis.byId[id]));
|
||||
if (loaded.length) {
|
||||
loaded.forEach((id) => INPUT_WAITING_CUSTOM_EMOJI_IDS.delete(id));
|
||||
processMessageInputForCustomEmoji();
|
||||
}
|
||||
}
|
||||
|
||||
export function getCustomEmojiMediaDataForInput(emojiId: string, isPreview?: boolean) {
|
||||
const mediaHash = isPreview ? getStickerPreviewHash(emojiId) : `sticker${emojiId}`;
|
||||
const data = mediaLoader.getFromMemory(mediaHash);
|
||||
if (data) {
|
||||
notifyCustomEmojiRender(emojiId);
|
||||
return data;
|
||||
}
|
||||
|
||||
mediaLoader.fetch(mediaHash, ApiMediaFormat.BlobUrl).then(() => processMessageInputForCustomEmoji());
|
||||
fetchAndProcess(mediaHash);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function fetchAndProcess(mediaHash: string) {
|
||||
return mediaLoader.fetch(mediaHash, ApiMediaFormat.BlobUrl).then(() => {
|
||||
processMessageInputForCustomEmoji();
|
||||
});
|
||||
}
|
||||
|
||||
export function getInputCustomEmojiParams(customEmoji?: ApiSticker) {
|
||||
if (!customEmoji) return [true, placeholderSrc, undefined];
|
||||
const shouldUseStaticFallback = !IS_WEBM_SUPPORTED && customEmoji.isVideo;
|
||||
const isUsingSharedCanvas = customEmoji.isLottie || (customEmoji.isVideo && !shouldUseStaticFallback);
|
||||
if (isUsingSharedCanvas) {
|
||||
fetchAndProcess(`sticker${customEmoji.id}`);
|
||||
return [false, blankSrc, generateIdFor(ID_STORE, true)];
|
||||
}
|
||||
|
||||
const mediaData = getCustomEmojiMediaDataForInput(customEmoji.id, shouldUseStaticFallback);
|
||||
|
||||
return [!mediaData, mediaData || placeholderSrc, undefined];
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user