From b1a2e27576b3bbcc5d319a506540042462e8b105 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sun, 9 May 2021 03:56:48 +0300 Subject: [PATCH] Revert "Composer: Add Emoji Picker (#1061)" This reverts commit 70c8cd2ca8fc07f9bc005018e6a71df8027e0eb4. --- package-lock.json | 4 +- package.json | 2 +- src/@types/global.d.ts | 2 +- src/bundles/extra.ts | 5 +- .../middle/composer/AttachmentModal.scss | 2 +- .../middle/composer/AttachmentModal.tsx | 16 +- src/components/middle/composer/Composer.scss | 34 ----- src/components/middle/composer/Composer.tsx | 48 ++---- .../middle/composer/EmojiButton.scss | 1 - .../middle/composer/EmojiButton.tsx | 7 +- .../middle/composer/EmojiTooltip.scss | 40 ++++- .../middle/composer/EmojiTooltip.tsx | 142 ++++++------------ .../middle/composer/MentionMenu.async.tsx | 15 ++ .../{MentionTooltip.scss => MentionMenu.scss} | 31 +++- .../{MentionTooltip.tsx => MentionMenu.tsx} | 8 +- .../middle/composer/MentionTooltip.async.tsx | 15 -- .../middle/composer/MessageInput.tsx | 5 +- .../middle/composer/StickerTooltip.async.tsx | 15 -- .../middle/composer/StickerTooltip.scss | 10 -- .../middle/composer/StickerTooltip.tsx | 100 ------------ .../middle/composer/hooks/useEmojiTooltip.ts | 136 +++-------------- ...useMentionTooltip.ts => useMentionMenu.ts} | 6 +- .../composer/hooks/useStickerTooltip.ts | 36 ----- src/util/captureKeyboardListeners.ts | 7 +- src/util/emoji.ts | 14 +- src/util/fastSmoothScrollHorizontal.ts | 2 +- src/util/findInViewport.ts | 9 +- src/util/isFullyVisible.ts | 10 +- 28 files changed, 202 insertions(+), 520 deletions(-) create mode 100644 src/components/middle/composer/MentionMenu.async.tsx rename src/components/middle/composer/{MentionTooltip.scss => MentionMenu.scss} (50%) rename src/components/middle/composer/{MentionTooltip.tsx => MentionMenu.tsx} (96%) delete mode 100644 src/components/middle/composer/MentionTooltip.async.tsx delete mode 100644 src/components/middle/composer/StickerTooltip.async.tsx delete mode 100644 src/components/middle/composer/StickerTooltip.scss delete mode 100644 src/components/middle/composer/StickerTooltip.tsx rename src/components/middle/composer/hooks/{useMentionTooltip.ts => useMentionMenu.ts} (96%) delete mode 100644 src/components/middle/composer/hooks/useStickerTooltip.ts diff --git a/package-lock.json b/package-lock.json index f98f0c27c..00ad7a96e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7660,8 +7660,8 @@ "dev": true }, "emoji-data-ios": { - "version": "github:korenskoy/emoji-data-ios#e2c6557d2d36612a882d9b81b2467f441f1f4179", - "from": "github:korenskoy/emoji-data-ios#e2c6557" + "version": "github:korenskoy/emoji-data-ios#10073b1244de618a3e587ae1d91b5e46ec01fd06", + "from": "github:korenskoy/emoji-data-ios#10073b1" }, "emojis-list": { "version": "2.1.0", diff --git a/package.json b/package.json index d44a80737..42a5a785b 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "async-mutex": "^0.1.4", "big-integer": "painor/BigInteger.js", "croppie": "^2.6.4", - "emoji-data-ios": "github:korenskoy/emoji-data-ios#e2c6557", + "emoji-data-ios": "github:korenskoy/emoji-data-ios#10073b1", "events": "^3.0.0", "idb-keyval": "^5.0.5", "opus-recorder": "^6.2.0", diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index e5267aeaf..3257ce5b2 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -38,7 +38,7 @@ type EmojiCategory = { type Emoji = { id: string; - names: string[]; + colons: string; native: string; image: string; skin?: number; diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 021195619..cd775f2c7 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -27,13 +27,12 @@ export { default as AttachmentModal } from '../components/middle/composer/Attach export { default as PollModal } from '../components/middle/composer/PollModal'; export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu'; export { default as AttachMenu } from '../components/middle/composer/AttachMenu'; -export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip'; -export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip'; +export { default as MentionMenu } from '../components/middle/composer/MentionMenu'; +export { default as EmojiTooltip } from '../components/middle/composer/EmojiTooltip'; export { default as BotKeyboardMenu } from '../components/middle/composer/BotKeyboardMenu'; export { default as CustomSendMenu } from '../components/middle/composer/CustomSendMenu'; export { default as DropArea } from '../components/middle/composer/DropArea'; export { default as TextFormatter } from '../components/middle/composer/TextFormatter'; -export { default as EmojiTooltip } from '../components/middle/composer/EmojiTooltip'; export { default as RightSearch } from '../components/right/RightSearch'; export { default as StickerSearch } from '../components/right/StickerSearch'; diff --git a/src/components/middle/composer/AttachmentModal.scss b/src/components/middle/composer/AttachmentModal.scss index 52185daa5..b952891ca 100644 --- a/src/components/middle/composer/AttachmentModal.scss +++ b/src/components/middle/composer/AttachmentModal.scss @@ -69,7 +69,7 @@ background: var(--color-background); } - .MentionTooltip { + .MentionMenu { right: 0 !important; z-index: 0; } diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 96975070a..0568786ea 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -8,14 +8,14 @@ import { EDITABLE_INPUT_MODAL_ID } from '../../../config'; import { getFileExtension } from '../../common/helpers/documentInfo'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; import usePrevious from '../../../hooks/usePrevious'; -import useMentionTooltip from './hooks/useMentionTooltip'; +import useMentionMenu from './hooks/useMentionMenu'; import useLang from '../../../hooks/useLang'; import Button from '../../ui/Button'; import Modal from '../../ui/Modal'; import File from '../../common/File'; import MessageInput from './MessageInput'; -import MentionTooltip from './MentionTooltip'; +import MentionMenu from './MentionMenu'; import './AttachmentModal.scss'; @@ -47,10 +47,10 @@ const AttachmentModal: FC = ({ const isOpen = Boolean(attachments.length); const { - isMentionTooltipOpen, mentionFilter, - closeMentionTooltip, insertMention, + isMentionMenuOpen, mentionFilter, + closeMentionMenu, insertMention, mentionFilteredMembers, - } = useMentionTooltip( + } = useMentionMenu( canSuggestMembers && isOpen, caption, onCaptionUpdate, @@ -136,9 +136,9 @@ const AttachmentModal: FC = ({ )}
- ; - recentEmojis: string[]; lastSyncTime?: number; contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled']; shouldSuggestStickers?: boolean; @@ -174,7 +171,6 @@ const Composer: FC = ({ lastSyncTime, contentToBeScheduled, shouldSuggestStickers, - recentEmojis, sendMessage, editMessage, saveDraft, @@ -259,10 +255,10 @@ const Composer: FC = ({ const canShowCustomSendMenu = !shouldSchedule; const { - isMentionTooltipOpen, mentionFilter, - closeMentionTooltip, insertMention, + isMentionMenuOpen, mentionFilter, + closeMentionMenu, insertMention, mentionFilteredMembers, - } = useMentionTooltip( + } = useMentionMenu( canSuggestMembers && !attachments.length, html, setHtml, @@ -286,20 +282,11 @@ const Composer: FC = ({ const isAdmin = chat && isChatAdmin(chat); const slowMode = getChatSlowModeOptions(chat); - const { isStickerTooltipOpen, closeStickerTooltip } = useStickerTooltip( + const { isEmojiTooltipOpen, closeEmojiTooltip } = useEmojiTooltip( Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length), html, stickersForEmoji, ); - const { - isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji, - } = useEmojiTooltip( - Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length), - html, - recentEmojis, - undefined, - setHtml, - ); const insertTextAndUpdateCursor = useCallback((text: string) => { const selection = window.getSelection()!; @@ -351,11 +338,10 @@ const Composer: FC = ({ const resetComposer = useCallback(() => { setHtml(''); setAttachments([]); - closeStickerTooltip(); + closeEmojiTooltip(); closeCalendar(); setScheduledMessageArgs(undefined); - closeMentionTooltip(); - closeEmojiTooltip(); + closeMentionMenu(); if (IS_MOBILE_SCREEN) { // @perf @@ -363,7 +349,7 @@ const Composer: FC = ({ } else { closeSymbolMenu(); } - }, [closeStickerTooltip, closeCalendar, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu]); + }, [closeEmojiTooltip, closeCalendar, closeMentionMenu, closeSymbolMenu]); // Handle chat change const prevChatId = usePrevious(chatId); @@ -704,10 +690,10 @@ const Composer: FC = ({ message={renderedEditedMessage} /> )} - = ({ } shouldSetFocus={isSymbolMenuOpen} shouldSupressFocus={IS_MOBILE_SCREEN && isSymbolMenuOpen} - shouldSupressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen} onUpdate={setHtml} onSend={mainButtonState === MainButtonState.Edit ? handleEditComplete @@ -802,15 +787,9 @@ const Composer: FC = ({ {formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)} )} - ( isPaymentModalOpen: global.payment.isPaymentModalOpen, isReceiptModalOpen: Boolean(global.payment.receipt), shouldSuggestStickers: global.settings.byKey.shouldSuggestStickers, - recentEmojis: global.recentEmojis, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ diff --git a/src/components/middle/composer/EmojiButton.scss b/src/components/middle/composer/EmojiButton.scss index 6e20ae65b..b3f4e6d10 100644 --- a/src/components/middle/composer/EmojiButton.scss +++ b/src/components/middle/composer/EmojiButton.scss @@ -16,7 +16,6 @@ line-height: inherit; } - &.focus, &:hover { background-color: rgba(var(--color-text-secondary-rgb), 0.08); } diff --git a/src/components/middle/composer/EmojiButton.tsx b/src/components/middle/composer/EmojiButton.tsx index d2c0e0b5e..687fb296d 100644 --- a/src/components/middle/composer/EmojiButton.tsx +++ b/src/components/middle/composer/EmojiButton.tsx @@ -6,20 +6,19 @@ import './EmojiButton.scss'; type OwnProps = { emoji: Emoji; - focus?: boolean; onClick: (emoji: string, name: string) => void; }; -const EmojiButton: FC = ({ emoji, focus, onClick }) => { +const EmojiButton: FC = ({ emoji, onClick }) => { const handleClick = useCallback(() => { onClick(emoji.native, emoji.id); }, [emoji, onClick]); return (
{IS_EMOJI_SUPPORTED ? emoji.native : }
diff --git a/src/components/middle/composer/EmojiTooltip.scss b/src/components/middle/composer/EmojiTooltip.scss index 817f5e818..15314fe9b 100644 --- a/src/components/middle/composer/EmojiTooltip.scss +++ b/src/components/middle/composer/EmojiTooltip.scss @@ -1,11 +1,37 @@ .EmojiTooltip { - display: flex; - padding-left: .25rem; - overflow-x: auto; - overflow-x: overlay; - overflow-y: hidden; + position: absolute; + bottom: calc(100% + .5rem); + left: 0; + width: 100%; + background: var(--color-background); + border-radius: var(--border-radius-messages); + padding: 0.5rem 0; + max-height: 15rem; + overflow-x: hidden; + overflow-y: auto; - .EmojiButton { - flex: 0 0 2.5rem + display: grid; + grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr)); + grid-auto-rows: auto; + place-items: center; + + box-shadow: 0 1px 2px var(--color-default-shadow); + + opacity: 0; + transform: translateY(1.5rem); + transform-origin: bottom; + transition: opacity var(--layer-transition), transform var(--layer-transition); + + &:not(.shown) { + display: none; + } + + &.open { + opacity: 1; + transform: translateY(0); + } + + .Loading { + margin: 1rem 0; } } diff --git a/src/components/middle/composer/EmojiTooltip.tsx b/src/components/middle/composer/EmojiTooltip.tsx index 47fb1fe2d..ce37716ec 100644 --- a/src/components/middle/composer/EmojiTooltip.tsx +++ b/src/components/middle/composer/EmojiTooltip.tsx @@ -1,110 +1,55 @@ import React, { - FC, memo, useCallback, useEffect, useRef, useState, + FC, memo, useEffect, useRef, } from '../../../lib/teact/teact'; +import { withGlobal } from '../../../lib/teact/teactn'; +import { ApiSticker } from '../../../api/types'; +import { GlobalActions } from '../../../global/types'; + +import { STICKER_SIZE_PICKER } from '../../../config'; import { IS_TOUCH_ENV } from '../../../util/environment'; import buildClassName from '../../../util/buildClassName'; -import cycleRestrict from '../../../util/cycleRestrict'; -import captureKeyboardListeners from '../../../util/captureKeyboardListeners'; -import findInViewport from '../../../util/findInViewport'; -import isFullyVisible from '../../../util/isFullyVisible'; -import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal'; +import captureEscKeyListener from '../../../util/captureEscKeyListener'; +import { pick } from '../../../util/iteratees'; +import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import useShowTransition from '../../../hooks/useShowTransition'; +import usePrevious from '../../../hooks/usePrevious'; import Loading from '../../ui/Loading'; -import EmojiButton from './EmojiButton'; +import StickerButton from '../../common/StickerButton'; import './EmojiTooltip.scss'; -const VIEWPORT_MARGIN = 8; -const EMOJI_BUTTON_WIDTH = 44; - -function setItemVisible(index: number, containerRef: Record) { - const container = containerRef.current!; - if (!container) { - return; - } - - const { visibleIndexes, allElements } = findInViewport( - container, - '.EmojiButton', - VIEWPORT_MARGIN, - true, - true, - true, - ); - - if (!allElements.length || !allElements[index]) { - return; - } - const first = visibleIndexes[0]; - if (!visibleIndexes.includes(index) - || (index === first && !isFullyVisible(container, allElements[first], true))) { - const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end'; - const newLeft = position === 'start' ? index * EMOJI_BUTTON_WIDTH : 0; - - fastSmoothScrollHorizontal(container, newLeft); - } -} - export type OwnProps = { isOpen: boolean; - onEmojiSelect: (text: string) => void; - onClose: NoneToVoidFunction; - emojis: Emoji[]; + onStickerSelect: (sticker: ApiSticker) => void; }; -const EmojiTooltip: FC = ({ +type StateProps = { + stickers?: ApiSticker[]; +}; + +type DispatchProps = Pick; + +const INTERSECTION_THROTTLE = 200; + +const EmojiTooltip: FC = ({ isOpen, - emojis, - onClose, - onEmojiSelect, + onStickerSelect, + stickers, + clearStickersForEmoji, }) => { // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); + const prevStickers = usePrevious(stickers, true); + const displayedStickers = stickers || prevStickers; - const [selectedIndex, setSelectedIndex] = useState(-1); + const { + observe: observeIntersection, + } = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE }); - useEffect(() => { - setSelectedIndex(0); - }, [emojis]); - - useEffect(() => { - setItemVisible(selectedIndex, containerRef); - }, [selectedIndex]); - - const getSelectedIndex = useCallback((newIndex: number) => { - if (!emojis.length) { - return -1; - } - - const emojisCount = emojis.length; - return cycleRestrict(emojisCount, newIndex); - }, [emojis]); - - - const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => { - e.preventDefault(); - setSelectedIndex((index) => (getSelectedIndex(index + value))); - }, [setSelectedIndex, getSelectedIndex]); - - const handleSelectEmoji = useCallback((e: KeyboardEvent) => { - if (emojis.length && selectedIndex > -1) { - const emoji = emojis[selectedIndex]; - if (emoji) { - e.preventDefault(); - onEmojiSelect(emoji.native); - } - } - }, [emojis, onEmojiSelect, selectedIndex]); - - useEffect(() => (isOpen ? captureKeyboardListeners({ - onEsc: onClose, - onLeft: (e: KeyboardEvent) => handleArrowKey(-1, e), - onRight: (e: KeyboardEvent) => handleArrowKey(1, e), - onEnter: handleSelectEmoji, - }) : undefined), [handleArrowKey, handleSelectEmoji, isOpen, onClose]); + useEffect(() => (isOpen ? captureEscKeyListener(clearStickersForEmoji) : undefined), [isOpen, clearStickersForEmoji]); const handleMouseEnter = () => { document.body.classList.add('no-select'); @@ -115,7 +60,7 @@ const EmojiTooltip: FC = ({ }; const className = buildClassName( - 'EmojiTooltip composer-tooltip custom-scroll-x', + 'EmojiTooltip custom-scroll', transitionClassNames, ); @@ -126,13 +71,15 @@ const EmojiTooltip: FC = ({ onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined} onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined} > - {shouldRender && emojis ? ( - emojis.map((emoji, index) => ( - ( + )) ) : shouldRender ? ( @@ -142,4 +89,11 @@ const EmojiTooltip: FC = ({ ); }; -export default memo(EmojiTooltip); +export default memo(withGlobal( + (global): StateProps => { + const { stickers } = global.stickers.forEmoji; + + return { stickers }; + }, + (setGlobal, actions): DispatchProps => pick(actions, ['clearStickersForEmoji']), +)(EmojiTooltip)); diff --git a/src/components/middle/composer/MentionMenu.async.tsx b/src/components/middle/composer/MentionMenu.async.tsx new file mode 100644 index 000000000..01e99dab8 --- /dev/null +++ b/src/components/middle/composer/MentionMenu.async.tsx @@ -0,0 +1,15 @@ +import React, { FC, memo } from '../../../lib/teact/teact'; +import { OwnProps } from './MentionMenu'; +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const MentionMenuAsync: FC = (props) => { + const { isOpen } = props; + const MentionMenu = useModuleLoader(Bundles.Extra, 'MentionMenu', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return MentionMenu ? : undefined; +}; + +export default memo(MentionMenuAsync); diff --git a/src/components/middle/composer/MentionTooltip.scss b/src/components/middle/composer/MentionMenu.scss similarity index 50% rename from src/components/middle/composer/MentionTooltip.scss rename to src/components/middle/composer/MentionMenu.scss index bec4e108f..a96fa9c2b 100644 --- a/src/components/middle/composer/MentionTooltip.scss +++ b/src/components/middle/composer/MentionMenu.scss @@ -1,14 +1,41 @@ -.MentionTooltip { +.MentionMenu { + position: absolute; + bottom: calc(100% + .75rem); + left: 0; width: calc(100% - 4rem); max-width: 20rem; - flex-direction: column; + background: var(--color-background); + border-radius: var(--border-radius-messages); + padding: 0.5rem 0; + max-height: 15rem; + overflow-x: hidden; + overflow-y: auto; + box-shadow: 3px 3px 5px var(--color-default-shadow); z-index: -1; + opacity: 0; + transform: translateY(1.5rem); + transform-origin: bottom; + transition: opacity var(--layer-transition), transform var(--layer-transition); + @media (max-width: 600px) { width: calc(100% - 3rem); } + &:not(.shown) { + display: none; + } + + &.open { + opacity: 1; + transform: translateY(0); + } + + .Loading { + margin: 1rem 0; + } + .ListItem.chat-item-clickable { margin: 0; diff --git a/src/components/middle/composer/MentionTooltip.tsx b/src/components/middle/composer/MentionMenu.tsx similarity index 96% rename from src/components/middle/composer/MentionTooltip.tsx rename to src/components/middle/composer/MentionMenu.tsx index face38747..73e3d58d7 100644 --- a/src/components/middle/composer/MentionTooltip.tsx +++ b/src/components/middle/composer/MentionMenu.tsx @@ -16,7 +16,7 @@ import cycleRestrict from '../../../util/cycleRestrict'; import ListItem from '../../ui/ListItem'; import PrivateChatInfo from '../../common/PrivateChatInfo'; -import './MentionTooltip.scss'; +import './MentionMenu.scss'; const VIEWPORT_MARGIN = 8; const SCROLL_MARGIN = 10; @@ -53,7 +53,7 @@ export type OwnProps = { usersById?: Record; }; -const MentionTooltip: FC = ({ +const MentionMenu: FC = ({ isOpen, filter, onClose, @@ -136,7 +136,7 @@ const MentionTooltip: FC = ({ } const className = buildClassName( - 'MentionTooltip composer-tooltip custom-scroll', + 'MentionMenu custom-scroll', transitionClassNames, ); @@ -160,4 +160,4 @@ const MentionTooltip: FC = ({ ); }; -export default memo(MentionTooltip); +export default memo(MentionMenu); diff --git a/src/components/middle/composer/MentionTooltip.async.tsx b/src/components/middle/composer/MentionTooltip.async.tsx deleted file mode 100644 index 228d9a30b..000000000 --- a/src/components/middle/composer/MentionTooltip.async.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React, { FC, memo } from '../../../lib/teact/teact'; -import { OwnProps } from './MentionTooltip'; -import { Bundles } from '../../../util/moduleLoader'; - -import useModuleLoader from '../../../hooks/useModuleLoader'; - -const MentionTooltipAsync: FC = (props) => { - const { isOpen } = props; - const MentionTooltip = useModuleLoader(Bundles.Extra, 'MentionTooltip', !isOpen); - - // eslint-disable-next-line react/jsx-props-no-spreading - return MentionTooltip ? : undefined; -}; - -export default memo(MentionTooltipAsync); diff --git a/src/components/middle/composer/MessageInput.tsx b/src/components/middle/composer/MessageInput.tsx index adcabd8c3..7caf41861 100644 --- a/src/components/middle/composer/MessageInput.tsx +++ b/src/components/middle/composer/MessageInput.tsx @@ -36,7 +36,6 @@ type OwnProps = { placeholder: string; shouldSetFocus: boolean; shouldSupressFocus?: boolean; - shouldSupressTextFormatter?: boolean; onUpdate: (html: string) => void; onSupressedFocus?: () => void; onSend: () => void; @@ -77,7 +76,6 @@ const MessageInput: FC = ({ placeholder, shouldSetFocus, shouldSupressFocus, - shouldSupressTextFormatter, onUpdate, onSupressedFocus, onSend, @@ -141,8 +139,7 @@ const MessageInput: FC = ({ const selectionRange = selection.getRangeAt(0); const selectedText = selectionRange.toString().trim(); if ( - shouldSupressTextFormatter - || !isSelectionInsideInput(selectionRange) + !isSelectionInsideInput(selectionRange) || !selectedText || parseEmojiOnlyString(selectedText) || !selectionRange.START_TO_END diff --git a/src/components/middle/composer/StickerTooltip.async.tsx b/src/components/middle/composer/StickerTooltip.async.tsx deleted file mode 100644 index 3a100816f..000000000 --- a/src/components/middle/composer/StickerTooltip.async.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React, { FC } from '../../../lib/teact/teact'; -import { OwnProps } from './StickerTooltip'; -import { Bundles } from '../../../util/moduleLoader'; - -import useModuleLoader from '../../../hooks/useModuleLoader'; - -const StickerTooltipAsync: FC = (props) => { - const { isOpen } = props; - const StickerTooltip = useModuleLoader(Bundles.Extra, 'StickerTooltip', !isOpen); - - // eslint-disable-next-line react/jsx-props-no-spreading - return StickerTooltip ? : undefined; -}; - -export default StickerTooltipAsync; diff --git a/src/components/middle/composer/StickerTooltip.scss b/src/components/middle/composer/StickerTooltip.scss deleted file mode 100644 index b9c940d9a..000000000 --- a/src/components/middle/composer/StickerTooltip.scss +++ /dev/null @@ -1,10 +0,0 @@ -.StickerTooltip { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr)); - grid-auto-rows: auto; - place-items: center; - - &.hidden { - display: none; - } -} diff --git a/src/components/middle/composer/StickerTooltip.tsx b/src/components/middle/composer/StickerTooltip.tsx deleted file mode 100644 index d07946d6b..000000000 --- a/src/components/middle/composer/StickerTooltip.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { - FC, memo, useEffect, useRef, -} from '../../../lib/teact/teact'; -import { withGlobal } from '../../../lib/teact/teactn'; - -import { ApiSticker } from '../../../api/types'; -import { GlobalActions } from '../../../global/types'; - -import { STICKER_SIZE_PICKER } from '../../../config'; -import { IS_TOUCH_ENV } from '../../../util/environment'; -import buildClassName from '../../../util/buildClassName'; -import captureEscKeyListener from '../../../util/captureEscKeyListener'; -import { pick } from '../../../util/iteratees'; -import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; -import useShowTransition from '../../../hooks/useShowTransition'; -import usePrevious from '../../../hooks/usePrevious'; - -import Loading from '../../ui/Loading'; -import StickerButton from '../../common/StickerButton'; - -import './StickerTooltip.scss'; - -export type OwnProps = { - isOpen: boolean; - onStickerSelect: (sticker: ApiSticker) => void; -}; - -type StateProps = { - stickers?: ApiSticker[]; -}; - -type DispatchProps = Pick; - -const INTERSECTION_THROTTLE = 200; - -const StickerTooltip: FC = ({ - isOpen, - onStickerSelect, - stickers, - clearStickersForEmoji, -}) => { - // eslint-disable-next-line no-null/no-null - const containerRef = useRef(null); - const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); - const prevStickers = usePrevious(stickers, true); - const displayedStickers = stickers || prevStickers; - - const { - observe: observeIntersection, - } = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE }); - - useEffect(() => (isOpen ? captureEscKeyListener(clearStickersForEmoji) : undefined), [isOpen, clearStickersForEmoji]); - - const handleMouseEnter = () => { - document.body.classList.add('no-select'); - }; - - const handleMouseLeave = () => { - document.body.classList.remove('no-select'); - }; - - const className = buildClassName( - 'StickerTooltip composer-tooltip custom-scroll', - transitionClassNames, - !(displayedStickers && displayedStickers.length) && 'hidden', - ); - - return ( -
- {shouldRender && displayedStickers ? ( - displayedStickers.map((sticker) => ( - - )) - ) : shouldRender ? ( - - ) : undefined} -
- ); -}; - -export default memo(withGlobal( - (global): StateProps => { - const { stickers } = global.stickers.forEmoji; - - return { stickers }; - }, - (setGlobal, actions): DispatchProps => pick(actions, ['clearStickersForEmoji']), -)(StickerTooltip)); diff --git a/src/components/middle/composer/hooks/useEmojiTooltip.ts b/src/components/middle/composer/hooks/useEmojiTooltip.ts index a89e802fa..b55e29bd6 100644 --- a/src/components/middle/composer/hooks/useEmojiTooltip.ts +++ b/src/components/middle/composer/hooks/useEmojiTooltip.ts @@ -1,132 +1,36 @@ -import { - useCallback, useEffect, useMemo, useState, -} from '../../../../lib/teact/teact'; +import { useEffect } from '../../../../lib/teact/teact'; +import { getDispatch } from '../../../../lib/teact/teactn'; -import { EDITABLE_INPUT_ID } from '../../../../config'; -import { IS_MOBILE_SCREEN } from '../../../../util/environment'; -import { - EmojiData, EmojiModule, EmojiRawData, uncompressEmoji, -} from '../../../../util/emoji'; -import useFlag from '../../../../hooks/useFlag'; -import focusEditableElement from '../../../../util/focusEditableElement'; +import { ApiSticker } from '../../../../api/types'; -let emojiDataPromise: Promise; -let emojiRawData: EmojiRawData; -let emojiData: EmojiData; +import { IS_EMOJI_SUPPORTED } from '../../../../util/environment'; -const RE_NOT_EMOJI_SEARCH = /[^-:_a-z\d]+/i; -const EMOJIS_LIMIT = 50; +import parseEmojiOnlyString from '../../../common/helpers/parseEmojiOnlyString'; export default function useEmojiTooltip( isAllowed: boolean, html: string, - recentEmojiIds: string[], - inputId = EDITABLE_INPUT_ID, - onUpdateHtml: (html: string) => void, + stickers?: ApiSticker[], ) { - const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); - const [emojis, setEmojis] = useState([]); - const [filteredEmojis, setFilteredEmojis] = useState([]); - - const recentEmojis = useMemo( - () => { - if (!emojis && !recentEmojiIds.length) { - return []; - } - - return emojis.filter((emoji) => recentEmojiIds.includes(emoji.id)) as Emoji[]; - }, - [emojis, recentEmojiIds], + const { loadStickersForEmoji, clearStickersForEmoji } = getDispatch(); + const isSingleEmoji = ( + (IS_EMOJI_SUPPORTED && parseEmojiOnlyString(html) === 1) + || (!IS_EMOJI_SUPPORTED && Boolean(html.match(/^]*?>$/g))) ); - - // Initialize data on first render. - useEffect(() => { - const exec = () => { - setEmojis(Object.values(emojiData.emojis)); - }; - - if (emojiData) { - exec(); - } else { - ensureEmojiData() - .then(exec); - } - }, []); + const hasStickers = Boolean(stickers) && isSingleEmoji; useEffect(() => { - if (!html || !emojis) { - unmarkIsOpen(); - return; + if (isAllowed && isSingleEmoji) { + loadStickersForEmoji({ emoji: html }); + } else if (hasStickers || !isSingleEmoji) { + clearStickersForEmoji(); } - - const code = getEmojiCode(html); - if (!code) { - setFilteredEmojis([]); - unmarkIsOpen(); - return; - } - - const filter = code.substr(1); - const matched = filter === '' ? recentEmojis : emojis.filter((emoji) => { - return 'names' in emoji && (!filter || emoji.names.find((name) => name.includes(filter))); - }) as Emoji[]; - - if (matched.length) { - markIsOpen(); - setFilteredEmojis(matched.slice(0, EMOJIS_LIMIT)); - } else { - unmarkIsOpen(); - } - }, [emojis, html, markIsOpen, recentEmojis, unmarkIsOpen]); - - const insertEmoji = useCallback((textEmoji: string) => { - const atIndex = html.lastIndexOf(':'); - if (atIndex !== -1) { - onUpdateHtml(`${html.substr(0, atIndex)}${textEmoji}`); - const messageInput = document.getElementById(inputId)!; - if (!IS_MOBILE_SCREEN) { - requestAnimationFrame(() => { - focusEditableElement(messageInput, true); - }); - } - } - - unmarkIsOpen(); - }, [html, inputId, onUpdateHtml, unmarkIsOpen]); + // We omit `hasStickers` here to prevent re-fetching after manually closing tooltip (via ). + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed]); return { - isEmojiTooltipOpen: isOpen, - closeEmojiTooltip: unmarkIsOpen, - filteredEmojis, - insertEmoji, + isEmojiTooltipOpen: hasStickers, + closeEmojiTooltip: clearStickersForEmoji, }; } - -function getEmojiCode(html: string) { - const tempEl = document.createElement('div'); - tempEl.innerHTML = html; - const text = tempEl.innerText; - - const lastSymbol = text[text.length - 1]; - const lastWord = text.split(RE_NOT_EMOJI_SEARCH).pop(); - - if ( - !text.length || RE_NOT_EMOJI_SEARCH.test(lastSymbol) - || !lastWord || !lastWord.startsWith(':') - ) { - return undefined; - } - - return lastWord.toLowerCase(); -} - -async function ensureEmojiData() { - if (!emojiDataPromise) { - emojiDataPromise = import('emoji-data-ios/emoji-data.json') as unknown as Promise; - emojiRawData = (await emojiDataPromise).default; - - emojiData = uncompressEmoji(emojiRawData); - } - - return emojiDataPromise; -} diff --git a/src/components/middle/composer/hooks/useMentionTooltip.ts b/src/components/middle/composer/hooks/useMentionMenu.ts similarity index 96% rename from src/components/middle/composer/hooks/useMentionTooltip.ts rename to src/components/middle/composer/hooks/useMentionMenu.ts index 7da685e25..67f43e60a 100644 --- a/src/components/middle/composer/hooks/useMentionTooltip.ts +++ b/src/components/middle/composer/hooks/useMentionMenu.ts @@ -10,7 +10,7 @@ import useFlag from '../../../../hooks/useFlag'; const RE_NOT_USERNAME_SEARCH = /[^@_\d\wа-яё]+/i; -export default function useMentionTooltip( +export default function useMentionMenu( canSuggestMembers: boolean | undefined, html: string, onUpdateHtml: (html: string) => void, @@ -90,9 +90,9 @@ export default function useMentionTooltip( }, [html, inputId, onUpdateHtml, unmarkIsOpen]); return { - isMentionTooltipOpen: isOpen, + isMentionMenuOpen: isOpen, mentionFilter: currentFilter, - closeMentionTooltip: unmarkIsOpen, + closeMentionMenu: unmarkIsOpen, insertMention, mentionFilteredMembers: filteredMembers, }; diff --git a/src/components/middle/composer/hooks/useStickerTooltip.ts b/src/components/middle/composer/hooks/useStickerTooltip.ts deleted file mode 100644 index 1f85f4466..000000000 --- a/src/components/middle/composer/hooks/useStickerTooltip.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { useEffect } from '../../../../lib/teact/teact'; -import { getDispatch } from '../../../../lib/teact/teactn'; - -import { ApiSticker } from '../../../../api/types'; - -import { IS_EMOJI_SUPPORTED } from '../../../../util/environment'; - -import parseEmojiOnlyString from '../../../common/helpers/parseEmojiOnlyString'; - -export default function useStickerTooltip( - isAllowed: boolean, - html: string, - stickers?: ApiSticker[], -) { - const { loadStickersForEmoji, clearStickersForEmoji } = getDispatch(); - const isSingleEmoji = ( - (IS_EMOJI_SUPPORTED && parseEmojiOnlyString(html) === 1) - || (!IS_EMOJI_SUPPORTED && Boolean(html.match(/^]*?>$/g))) - ); - const hasStickers = Boolean(stickers) && isSingleEmoji; - - useEffect(() => { - if (isAllowed && isSingleEmoji) { - loadStickersForEmoji({ emoji: html }); - } else if (hasStickers || !isSingleEmoji) { - clearStickersForEmoji(); - } - // We omit `hasStickers` here to prevent re-fetching after manually closing tooltip (via ). - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed]); - - return { - isStickerTooltipOpen: hasStickers, - closeStickerTooltip: clearStickersForEmoji, - }; -} diff --git a/src/util/captureKeyboardListeners.ts b/src/util/captureKeyboardListeners.ts index cef332fb3..8a087c335 100644 --- a/src/util/captureKeyboardListeners.ts +++ b/src/util/captureKeyboardListeners.ts @@ -1,5 +1,4 @@ -type HandlerName = 'onEnter' | 'onBackspace' | 'onDelete' | 'onEsc' | 'onUp' | 'onDown' | 'onLeft' | 'onRight' -| 'onTab'; +type HandlerName = 'onEnter' | 'onBackspace' | 'onDelete' | 'onEsc' | 'onUp' | 'onDown' | 'onTab'; type Handler = (e: KeyboardEvent) => void; type CaptureOptions = Partial>; @@ -11,8 +10,6 @@ const keyToHandlerName: Record = { Escape: 'onEsc', ArrowUp: 'onUp', ArrowDown: 'onDown', - ArrowLeft: 'onLeft', - ArrowRight: 'onRight', Tab: 'onTab', }; @@ -23,8 +20,6 @@ const handlers: Record = { onEsc: [], onUp: [], onDown: [], - onLeft: [], - onRight: [], onTab: [], }; diff --git a/src/util/emoji.ts b/src/util/emoji.ts index dbfa5906b..8dc9e5547 100644 --- a/src/util/emoji.ts +++ b/src/util/emoji.ts @@ -55,13 +55,13 @@ export function uncompressEmoji(data: EmojiRawData): EmojiData { for (let j = 0; j < data[i + 1].length; j++) { const emojiRaw = data[i + 1][j]; - if (!EXCLUDE_EMOJIS.includes(emojiRaw[1][0])) { - category.emojis.push(emojiRaw[1][0]); - emojiData.emojis[emojiRaw[1][0]] = { - id: emojiRaw[1][0], - names: emojiRaw[1] as string[], - native: unifiedToNative(emojiRaw[0] as string), - image: (emojiRaw[0] as string).toLowerCase(), + if (!EXCLUDE_EMOJIS.includes(emojiRaw[1])) { + category.emojis.push(emojiRaw[1]); + emojiData.emojis[emojiRaw[1]] = { + id: emojiRaw[1], + colons: `:${emojiRaw[1]}:`, + native: unifiedToNative(emojiRaw[0]), + image: emojiRaw[0].toLowerCase(), }; } } diff --git a/src/util/fastSmoothScrollHorizontal.ts b/src/util/fastSmoothScrollHorizontal.ts index 491fb23ee..5050b3a50 100644 --- a/src/util/fastSmoothScrollHorizontal.ts +++ b/src/util/fastSmoothScrollHorizontal.ts @@ -4,7 +4,7 @@ import { IS_IOS } from './environment'; const DURATION = 450; -export default function fastSmoothScrollHorizontal(container: HTMLElement, left: number) { +export default function fastSmoothScroll(container: HTMLElement, left: number) { // Native way seems to be smoother in Chrome if (!IS_IOS) { container.scrollTo({ left, behavior: 'smooth' }); diff --git a/src/util/findInViewport.ts b/src/util/findInViewport.ts index 9d81f132d..b4ff2166a 100644 --- a/src/util/findInViewport.ts +++ b/src/util/findInViewport.ts @@ -4,10 +4,9 @@ export default function findInViewport( margin = 0, isDense = false, shouldContainBottom = false, - isHorizontal = false, ) { - const viewportY1 = container[isHorizontal ? 'scrollLeft' : 'scrollTop']; - const viewportY2 = viewportY1 + container[isHorizontal ? 'offsetWidth' : 'offsetHeight']; + const viewportY1 = container.scrollTop; + const viewportY2 = viewportY1 + container.offsetHeight; const allElements = typeof selectorOrElements === 'string' ? container.querySelectorAll(selectorOrElements) : selectorOrElements; @@ -17,8 +16,8 @@ export default function findInViewport( for (let i = 0; i < length; i++) { const element = allElements[i]; - const y1 = element[isHorizontal ? 'offsetLeft' : 'offsetTop']; - const y2 = y1 + element[isHorizontal ? 'offsetWidth' : 'offsetHeight']; + const y1 = element.offsetTop; + const y2 = y1 + element.offsetHeight; const isVisible = shouldContainBottom ? y2 >= viewportY1 - margin && y2 <= viewportY2 + margin : y1 <= viewportY2 + margin && y2 >= viewportY1 - margin; diff --git a/src/util/isFullyVisible.ts b/src/util/isFullyVisible.ts index ee0a90acf..7cdeed367 100644 --- a/src/util/isFullyVisible.ts +++ b/src/util/isFullyVisible.ts @@ -1,8 +1,8 @@ -function isFullyVisible(container: HTMLElement, element: HTMLElement, isHorizontal = false) { - const viewportY1 = container[isHorizontal ? 'scrollLeft' : 'scrollTop']; - const viewportY2 = viewportY1 + container[isHorizontal ? 'offsetWidth' : 'offsetHeight']; - const y1 = element[isHorizontal ? 'offsetLeft' : 'offsetTop']; - const y2 = y1 + element[isHorizontal ? 'offsetWidth' : 'offsetHeight']; +function isFullyVisible(container: HTMLElement, element: HTMLElement) { + const viewportY1 = container.scrollTop; + const viewportY2 = viewportY1 + container.offsetHeight; + const y1 = element.offsetTop; + const y2 = y1 + element.offsetHeight; return y1 > viewportY1 && y2 < viewportY2; }