From a7ebcaf6646b894b8007bab3821e1abb614b2a3f Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 6 Dec 2022 13:29:44 +0100 Subject: [PATCH] Composer: Support animated emojis in input (#2143) --- src/components/common/CustomEmoji.tsx | 2 - src/components/common/hooks/useCustomEmoji.ts | 22 +- src/components/middle/MessageList.tsx | 23 +- .../middle/composer/AttachMenu.scss | 4 +- .../middle/composer/AttachmentModal.scss | 1 + src/components/middle/composer/Composer.scss | 103 +++++---- src/components/middle/composer/Composer.tsx | 2 +- .../middle/composer/MessageInput.tsx | 128 ++++++----- .../middle/composer/StickerSetCover.tsx | 15 +- .../middle/composer/SymbolMenuFooter.tsx | 2 +- .../middle/composer/TextFormatter.scss | 1 + .../middle/composer/helpers/customEmoji.ts | 21 +- .../middle/composer/hooks/useEditing.ts | 4 +- .../composer/hooks/useInputCustomEmojis.ts | 198 ++++++++++++++++++ src/components/ui/Spinner.tsx | 7 +- src/hooks/useBoundsInSharedCanvas.ts | 26 +-- src/hooks/useEnsureCustomEmoji.ts | 5 +- src/hooks/useResizeObserver.ts | 32 +++ src/lib/rlottie/RLottie.ts | 10 +- src/styles/index.scss | 2 +- src/util/AbsoluteVideo.ts | 71 +++++++ src/util/customEmojiManager.ts | 111 ++++++++-- 22 files changed, 604 insertions(+), 186 deletions(-) create mode 100644 src/components/middle/composer/hooks/useInputCustomEmojis.ts create mode 100644 src/hooks/useResizeObserver.ts create mode 100644 src/util/AbsoluteVideo.ts diff --git a/src/components/common/CustomEmoji.tsx b/src/components/common/CustomEmoji.tsx index b7c76e18d..d604b2344 100644 --- a/src/components/common/CustomEmoji.tsx +++ b/src/components/common/CustomEmoji.tsx @@ -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 = ({ // 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); diff --git a/src/components/common/hooks/useCustomEmoji.ts b/src/components/common/hooks/useCustomEmoji.ts index 971c81cba..2fb47018d 100644 --- a/src/components/common/hooks/useCustomEmoji.ts +++ b/src/components/common/hooks/useCustomEmoji.ts @@ -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(); +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(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]); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index f0dd07442..a1dfdee45 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -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 = ({ }, [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(); diff --git a/src/components/middle/composer/AttachMenu.scss b/src/components/middle/composer/AttachMenu.scss index d099e371b..70627b9d5 100644 --- a/src/components/middle/composer/AttachMenu.scss +++ b/src/components/middle/composer/AttachMenu.scss @@ -1,5 +1,7 @@ .AttachMenu { - align-self: flex-end; + position: absolute; + right: 0; + bottom: 0; &--button { &:focus { diff --git a/src/components/middle/composer/AttachmentModal.scss b/src/components/middle/composer/AttachmentModal.scss index d8f3e4c3b..7dbd4643e 100644 --- a/src/components/middle/composer/AttachmentModal.scss +++ b/src/components/middle/composer/AttachmentModal.scss @@ -77,6 +77,7 @@ .drop-target { position: relative; + overflow: hidden; &::before, &::after { diff --git a/src/components/middle/composer/Composer.scss b/src/components/middle/composer/Composer.scss index 17de952af..18ce772c6 100644 --- a/src/components/middle/composer/Composer.scss +++ b/src/components/middle/composer/Composer.scss @@ -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 { diff --git a/src/components/middle/composer/Composer.tsx b/src/components/middle/composer/Composer.tsx index 1515c5cb7..002ace436 100644 --- a/src/components/middle/composer/Composer.tsx +++ b/src/components/middle/composer/Composer.tsx @@ -1261,7 +1261,7 @@ const Composer: FC = ({ onSuppressedFocus={closeSymbolMenu} /> {isInlineBotLoading && Boolean(inlineBotId) && ( - + )} {withScheduledButton && (