diff --git a/src/components/middle/composer/CustomEmojiButton.tsx b/src/components/middle/composer/CustomEmojiButton.tsx index 5e31311de..290caa4c4 100644 --- a/src/components/middle/composer/CustomEmojiButton.tsx +++ b/src/components/middle/composer/CustomEmojiButton.tsx @@ -2,6 +2,7 @@ import React, { memo, useCallback } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact'; import type { ApiSticker } from '../../../api/types'; +import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import buildClassName from '../../../util/buildClassName'; @@ -15,10 +16,11 @@ type OwnProps = { emoji: ApiSticker; focus?: boolean; onClick?: (emoji: ApiSticker) => void; + observeIntersection?: ObserveFn; }; const CustomEmojiButton: FC = ({ - emoji, focus, onClick, + emoji, focus, onClick, observeIntersection, }) => { const handleClick = useCallback((e: React.MouseEvent) => { // Preventing safari from losing focus on Composer MessageInput @@ -38,7 +40,13 @@ const CustomEmojiButton: FC = ({ onMouseDown={handleClick} title={emoji.emoji} > - + ); }; diff --git a/src/components/middle/composer/CustomEmojiTooltip.module.scss b/src/components/middle/composer/CustomEmojiTooltip.module.scss index 06b55e577..287e589d7 100644 --- a/src/components/middle/composer/CustomEmojiTooltip.module.scss +++ b/src/components/middle/composer/CustomEmojiTooltip.module.scss @@ -10,7 +10,7 @@ } .emojiButton { - flex: 0 0 2.5rem; + flex: 0 0 2rem; --custom-emoji-size: 2rem; margin: 0.5rem 0 0.5rem 0.25rem; } diff --git a/src/components/middle/composer/CustomEmojiTooltip.tsx b/src/components/middle/composer/CustomEmojiTooltip.tsx index 275a03514..e39f2aa17 100644 --- a/src/components/middle/composer/CustomEmojiTooltip.tsx +++ b/src/components/middle/composer/CustomEmojiTooltip.tsx @@ -59,7 +59,7 @@ const CustomEmojiTooltip: FC = ({ const { observe: observeIntersection, - } = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE }); + } = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen }); useEffect(() => (isOpen ? captureEscKeyListener(onClose) : undefined), [isOpen, onClose]); diff --git a/src/components/middle/composer/EmojiTooltip.tsx b/src/components/middle/composer/EmojiTooltip.tsx index 8533867d9..a0146137a 100644 --- a/src/components/middle/composer/EmojiTooltip.tsx +++ b/src/components/middle/composer/EmojiTooltip.tsx @@ -14,6 +14,7 @@ import useShowTransition from '../../../hooks/useShowTransition'; import usePrevDuringAnimation from '../../../hooks/usePrevDuringAnimation'; import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'; import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; +import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import Loading from '../../ui/Loading'; import EmojiButton from './EmojiButton'; @@ -64,6 +65,8 @@ export type OwnProps = { addRecentCustomEmoji: ({ documentId }: { documentId: string }) => void; }; +const INTERSECTION_THROTTLE = 200; + const EmojiTooltip: FC = ({ isOpen, emojis, @@ -83,6 +86,10 @@ const EmojiTooltip: FC = ({ useHorizontalScroll(containerRef); + const { + observe: observeIntersection, + } = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen }); + const handleSelectEmoji = useCallback((emoji: Emoji) => { onEmojiSelect(emoji.native); addRecentEmoji({ emoji: emoji.id }); @@ -148,6 +155,7 @@ const EmojiTooltip: FC = ({ emoji={emoji} focus={selectedIndex === index} onClick={handleCustomEmojiClick} + observeIntersection={observeIntersection} /> ) )) diff --git a/src/components/middle/composer/hooks/useEmojiTooltip.ts b/src/components/middle/composer/hooks/useEmojiTooltip.ts index 2d96fa601..39007d26f 100644 --- a/src/components/middle/composer/hooks/useEmojiTooltip.ts +++ b/src/components/middle/composer/hooks/useEmojiTooltip.ts @@ -27,6 +27,7 @@ interface Library { byKeyword: Record; names: string[]; byName: Record; + maxKeyLength: number; } let emojiDataPromise: Promise; @@ -34,6 +35,7 @@ let emojiRawData: EmojiRawData; let emojiData: EmojiData; let RE_EMOJI_SEARCH: RegExp; +let RE_LOWERCASE_TEST: RegExp; const EMOJIS_LIMIT = 36; const FILTER_MIN_LENGTH = 2; @@ -44,10 +46,12 @@ const prepareLibraryMemo = memoized(prepareLibrary); const searchInLibraryMemo = memoized(searchInLibrary); try { - RE_EMOJI_SEARCH = /(^|\s):[-+_:\p{L}\p{N}]*$/gui; + RE_EMOJI_SEARCH = /(^|\s):(?!\s)[-+_:'\s\p{L}\p{N}]*$/gui; + RE_LOWERCASE_TEST = /\p{Ll}/u; } catch (e) { // Support for older versions of firefox - RE_EMOJI_SEARCH = /(^|\s):[-+_:\d\wа-яё]*$/gi; + RE_EMOJI_SEARCH = /(^|\s):(?!\s)[-+_:'\s\d\wа-яёґєії]*$/gi; + RE_LOWERCASE_TEST = /[a-zяёґєії]/; } export default function useEmojiTooltip( @@ -141,12 +145,13 @@ export default function useEmojiTooltip( if (!filter) { matched = prepareRecentEmojisMemo(byId, recentEmojiIds, EMOJIS_LIMIT); - } else if (filter.length >= FILTER_MIN_LENGTH) { + } else if ((filter.length === 1 && RE_LOWERCASE_TEST.test(filter)) || filter.length >= FILTER_MIN_LENGTH) { const library = prepareLibraryMemo(byId, baseEmojiKeywords, emojiKeywords); - matched = searchInLibraryMemo(library, filter, EMOJIS_LIMIT); + matched = searchInLibraryMemo(library, filter.toLowerCase(), EMOJIS_LIMIT); } if (!matched.length) { + updateFiltered(MEMO_EMPTY_ARRAY); return; } @@ -172,7 +177,7 @@ export default function useEmojiTooltip( async function ensureEmojiData() { if (!emojiDataPromise) { - emojiDataPromise = import('emoji-data-ios/emoji-data.json') as unknown as Promise; + emojiDataPromise = import('emoji-data-ios/emoji-data.json'); emojiRawData = (await emojiDataPromise).default; emojiData = uncompressEmoji(emojiRawData); @@ -224,21 +229,27 @@ function prepareLibrary( }, {} as Record); const names = Object.keys(byName); + const maxKeyLength = keywords.reduce((max, keyword) => Math.max(max, keyword.length), 0); return { byKeyword, keywords, byName, names, + maxKeyLength, }; } function searchInLibrary(library: Library, filter: string, limit: number) { const { - byKeyword, keywords, byName, names, + byKeyword, keywords, byName, names, maxKeyLength, } = library; - let matched: Emoji[] = MEMO_EMPTY_ARRAY; + let matched: Emoji[] = []; + + if (filter.length > maxKeyLength) { + return MEMO_EMPTY_ARRAY; + } const matchedKeywords = keywords.filter((keyword) => keyword.startsWith(filter)).sort(); matched = matched.concat(Object.values(pickTruthy(byKeyword!, matchedKeywords)).flat()); @@ -249,5 +260,9 @@ function searchInLibrary(library: Library, filter: string, limit: number) { matched = unique(matched); + if (!matched.length) { + return MEMO_EMPTY_ARRAY; + } + return matched.slice(0, limit); } diff --git a/src/components/middle/composer/hooks/useMentionTooltip.ts b/src/components/middle/composer/hooks/useMentionTooltip.ts index 8e8d79368..934d20480 100644 --- a/src/components/middle/composer/hooks/useMentionTooltip.ts +++ b/src/components/middle/composer/hooks/useMentionTooltip.ts @@ -25,7 +25,7 @@ try { RE_USERNAME_SEARCH = /(^|\s)@[-_\p{L}\p{M}\p{N}]*$/gui; } catch (e) { // Support for older versions of Firefox - RE_USERNAME_SEARCH = /(^|\s)@[-_\d\wа-яё]*$/gi; + RE_USERNAME_SEARCH = /(^|\s)@[-_\d\wа-яёґєії]*$/gi; } export default function useMentionTooltip( diff --git a/src/util/fastSmoothScrollHorizontal.ts b/src/util/fastSmoothScrollHorizontal.ts index 30ae9d5db..cc818cffc 100644 --- a/src/util/fastSmoothScrollHorizontal.ts +++ b/src/util/fastSmoothScrollHorizontal.ts @@ -71,7 +71,7 @@ function scrollWithJs(container: HTMLElement, left: number, duration: number) { if (t >= 1) { container.style.scrollSnapType = ''; - container.dataset.scrollId = undefined; + delete container.dataset.scrollId; stopById.delete(id); resolve(); } diff --git a/src/util/searchWords.ts b/src/util/searchWords.ts index 7800f952b..f64206ed3 100644 --- a/src/util/searchWords.ts +++ b/src/util/searchWords.ts @@ -4,7 +4,7 @@ try { RE_NOT_LETTER = /[^\p{L}\p{M}]+/ui; } catch (e) { // Support for older versions of firefox - RE_NOT_LETTER = /[^\wа-яё]+/i; + RE_NOT_LETTER = /[^\wа-яёґєії]+/i; } export default function searchWords(haystack: string, needle: string | string[]) {