Revert "Composer: Add Emoji Picker (#1061)"

This reverts commit 70c8cd2ca8fc07f9bc005018e6a71df8027e0eb4.
This commit is contained in:
Alexander Zinchuk 2021-05-09 03:56:48 +03:00
parent 3faf754ffc
commit b1a2e27576
28 changed files with 202 additions and 520 deletions

4
package-lock.json generated
View File

@ -7660,8 +7660,8 @@
"dev": true "dev": true
}, },
"emoji-data-ios": { "emoji-data-ios": {
"version": "github:korenskoy/emoji-data-ios#e2c6557d2d36612a882d9b81b2467f441f1f4179", "version": "github:korenskoy/emoji-data-ios#10073b1244de618a3e587ae1d91b5e46ec01fd06",
"from": "github:korenskoy/emoji-data-ios#e2c6557" "from": "github:korenskoy/emoji-data-ios#10073b1"
}, },
"emojis-list": { "emojis-list": {
"version": "2.1.0", "version": "2.1.0",

View File

@ -101,7 +101,7 @@
"async-mutex": "^0.1.4", "async-mutex": "^0.1.4",
"big-integer": "painor/BigInteger.js", "big-integer": "painor/BigInteger.js",
"croppie": "^2.6.4", "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", "events": "^3.0.0",
"idb-keyval": "^5.0.5", "idb-keyval": "^5.0.5",
"opus-recorder": "^6.2.0", "opus-recorder": "^6.2.0",

View File

@ -38,7 +38,7 @@ type EmojiCategory = {
type Emoji = { type Emoji = {
id: string; id: string;
names: string[]; colons: string;
native: string; native: string;
image: string; image: string;
skin?: number; skin?: number;

View File

@ -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 PollModal } from '../components/middle/composer/PollModal';
export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu'; export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu';
export { default as AttachMenu } from '../components/middle/composer/AttachMenu'; export { default as AttachMenu } from '../components/middle/composer/AttachMenu';
export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip'; export { default as MentionMenu } from '../components/middle/composer/MentionMenu';
export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip'; export { default as EmojiTooltip } from '../components/middle/composer/EmojiTooltip';
export { default as BotKeyboardMenu } from '../components/middle/composer/BotKeyboardMenu'; export { default as BotKeyboardMenu } from '../components/middle/composer/BotKeyboardMenu';
export { default as CustomSendMenu } from '../components/middle/composer/CustomSendMenu'; export { default as CustomSendMenu } from '../components/middle/composer/CustomSendMenu';
export { default as DropArea } from '../components/middle/composer/DropArea'; export { default as DropArea } from '../components/middle/composer/DropArea';
export { default as TextFormatter } from '../components/middle/composer/TextFormatter'; 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 RightSearch } from '../components/right/RightSearch';
export { default as StickerSearch } from '../components/right/StickerSearch'; export { default as StickerSearch } from '../components/right/StickerSearch';

View File

@ -69,7 +69,7 @@
background: var(--color-background); background: var(--color-background);
} }
.MentionTooltip { .MentionMenu {
right: 0 !important; right: 0 !important;
z-index: 0; z-index: 0;
} }

View File

@ -8,14 +8,14 @@ import { EDITABLE_INPUT_MODAL_ID } from '../../../config';
import { getFileExtension } from '../../common/helpers/documentInfo'; import { getFileExtension } from '../../common/helpers/documentInfo';
import captureEscKeyListener from '../../../util/captureEscKeyListener'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import useMentionTooltip from './hooks/useMentionTooltip'; import useMentionMenu from './hooks/useMentionMenu';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Modal from '../../ui/Modal'; import Modal from '../../ui/Modal';
import File from '../../common/File'; import File from '../../common/File';
import MessageInput from './MessageInput'; import MessageInput from './MessageInput';
import MentionTooltip from './MentionTooltip'; import MentionMenu from './MentionMenu';
import './AttachmentModal.scss'; import './AttachmentModal.scss';
@ -47,10 +47,10 @@ const AttachmentModal: FC<OwnProps> = ({
const isOpen = Boolean(attachments.length); const isOpen = Boolean(attachments.length);
const { const {
isMentionTooltipOpen, mentionFilter, isMentionMenuOpen, mentionFilter,
closeMentionTooltip, insertMention, closeMentionMenu, insertMention,
mentionFilteredMembers, mentionFilteredMembers,
} = useMentionTooltip( } = useMentionMenu(
canSuggestMembers && isOpen, canSuggestMembers && isOpen,
caption, caption,
onCaptionUpdate, onCaptionUpdate,
@ -136,9 +136,9 @@ const AttachmentModal: FC<OwnProps> = ({
)} )}
<div className="attachment-caption-wrapper"> <div className="attachment-caption-wrapper">
<MentionTooltip <MentionMenu
isOpen={isMentionTooltipOpen} isOpen={isMentionMenuOpen}
onClose={closeMentionTooltip} onClose={closeMentionMenu}
filter={mentionFilter} filter={mentionFilter}
onInsertUserName={insertMention} onInsertUserName={insertMention}
filteredChatMembers={mentionFilteredMembers} filteredChatMembers={mentionFilteredMembers}

View File

@ -368,37 +368,3 @@
left: 1rem; left: 1rem;
} }
} }
.composer-tooltip {
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;
overflow-y: overlay;
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;
}
}

View File

@ -55,9 +55,8 @@ import useClipboardPaste from './hooks/useClipboardPaste';
import useDraft from './hooks/useDraft'; import useDraft from './hooks/useDraft';
import useEditing from './hooks/useEditing'; import useEditing from './hooks/useEditing';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import useStickerTooltip from './hooks/useStickerTooltip';
import useEmojiTooltip from './hooks/useEmojiTooltip'; import useEmojiTooltip from './hooks/useEmojiTooltip';
import useMentionTooltip from './hooks/useMentionTooltip'; import useMentionMenu from './hooks/useMentionMenu';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -67,9 +66,8 @@ import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
import Spinner from '../../ui/Spinner'; import Spinner from '../../ui/Spinner';
import AttachMenu from './AttachMenu.async'; import AttachMenu from './AttachMenu.async';
import SymbolMenu from './SymbolMenu.async'; import SymbolMenu from './SymbolMenu.async';
import MentionTooltip from './MentionTooltip.async'; import MentionMenu from './MentionMenu.async';
import CustomSendMenu from './CustomSendMenu.async'; import CustomSendMenu from './CustomSendMenu.async';
import StickerTooltip from './StickerTooltip.async';
import EmojiTooltip from './EmojiTooltip.async'; import EmojiTooltip from './EmojiTooltip.async';
import BotKeyboardMenu from './BotKeyboardMenu.async'; import BotKeyboardMenu from './BotKeyboardMenu.async';
import MessageInput from './MessageInput'; import MessageInput from './MessageInput';
@ -114,7 +112,6 @@ type StateProps = {
groupChatMembers?: ApiChatMember[]; groupChatMembers?: ApiChatMember[];
currentUserId?: number; currentUserId?: number;
usersById?: Record<number, ApiUser>; usersById?: Record<number, ApiUser>;
recentEmojis: string[];
lastSyncTime?: number; lastSyncTime?: number;
contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled']; contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled'];
shouldSuggestStickers?: boolean; shouldSuggestStickers?: boolean;
@ -174,7 +171,6 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
lastSyncTime, lastSyncTime,
contentToBeScheduled, contentToBeScheduled,
shouldSuggestStickers, shouldSuggestStickers,
recentEmojis,
sendMessage, sendMessage,
editMessage, editMessage,
saveDraft, saveDraft,
@ -259,10 +255,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
const canShowCustomSendMenu = !shouldSchedule; const canShowCustomSendMenu = !shouldSchedule;
const { const {
isMentionTooltipOpen, mentionFilter, isMentionMenuOpen, mentionFilter,
closeMentionTooltip, insertMention, closeMentionMenu, insertMention,
mentionFilteredMembers, mentionFilteredMembers,
} = useMentionTooltip( } = useMentionMenu(
canSuggestMembers && !attachments.length, canSuggestMembers && !attachments.length,
html, html,
setHtml, setHtml,
@ -286,20 +282,11 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
const isAdmin = chat && isChatAdmin(chat); const isAdmin = chat && isChatAdmin(chat);
const slowMode = getChatSlowModeOptions(chat); const slowMode = getChatSlowModeOptions(chat);
const { isStickerTooltipOpen, closeStickerTooltip } = useStickerTooltip( const { isEmojiTooltipOpen, closeEmojiTooltip } = useEmojiTooltip(
Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length), Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length),
html, html,
stickersForEmoji, stickersForEmoji,
); );
const {
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
} = useEmojiTooltip(
Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length),
html,
recentEmojis,
undefined,
setHtml,
);
const insertTextAndUpdateCursor = useCallback((text: string) => { const insertTextAndUpdateCursor = useCallback((text: string) => {
const selection = window.getSelection()!; const selection = window.getSelection()!;
@ -351,11 +338,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
const resetComposer = useCallback(() => { const resetComposer = useCallback(() => {
setHtml(''); setHtml('');
setAttachments([]); setAttachments([]);
closeStickerTooltip(); closeEmojiTooltip();
closeCalendar(); closeCalendar();
setScheduledMessageArgs(undefined); setScheduledMessageArgs(undefined);
closeMentionTooltip(); closeMentionMenu();
closeEmojiTooltip();
if (IS_MOBILE_SCREEN) { if (IS_MOBILE_SCREEN) {
// @perf // @perf
@ -363,7 +349,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
} else { } else {
closeSymbolMenu(); closeSymbolMenu();
} }
}, [closeStickerTooltip, closeCalendar, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu]); }, [closeEmojiTooltip, closeCalendar, closeMentionMenu, closeSymbolMenu]);
// Handle chat change // Handle chat change
const prevChatId = usePrevious(chatId); const prevChatId = usePrevious(chatId);
@ -704,10 +690,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
message={renderedEditedMessage} message={renderedEditedMessage}
/> />
)} )}
<MentionTooltip <MentionMenu
isOpen={isMentionTooltipOpen} isOpen={isMentionMenuOpen}
filter={mentionFilter} filter={mentionFilter}
onClose={closeMentionTooltip} onClose={closeMentionMenu}
onInsertUserName={insertMention} onInsertUserName={insertMention}
filteredChatMembers={mentionFilteredMembers} filteredChatMembers={mentionFilteredMembers}
usersById={usersById} usersById={usersById}
@ -754,7 +740,6 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
} }
shouldSetFocus={isSymbolMenuOpen} shouldSetFocus={isSymbolMenuOpen}
shouldSupressFocus={IS_MOBILE_SCREEN && isSymbolMenuOpen} shouldSupressFocus={IS_MOBILE_SCREEN && isSymbolMenuOpen}
shouldSupressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen}
onUpdate={setHtml} onUpdate={setHtml}
onSend={mainButtonState === MainButtonState.Edit onSend={mainButtonState === MainButtonState.Edit
? handleEditComplete ? handleEditComplete
@ -802,15 +787,9 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
{formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)} {formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)}
</span> </span>
)} )}
<StickerTooltip
isOpen={isStickerTooltipOpen}
onStickerSelect={handleStickerSelect}
/>
<EmojiTooltip <EmojiTooltip
isOpen={isEmojiTooltipOpen} isOpen={isEmojiTooltipOpen}
emojis={filteredEmojis} onStickerSelect={handleStickerSelect}
onClose={closeEmojiTooltip}
onEmojiSelect={insertEmoji}
/> />
<AttachMenu <AttachMenu
isOpen={isAttachMenuOpen} isOpen={isAttachMenuOpen}
@ -932,7 +911,6 @@ export default memo(withGlobal<OwnProps>(
isPaymentModalOpen: global.payment.isPaymentModalOpen, isPaymentModalOpen: global.payment.isPaymentModalOpen,
isReceiptModalOpen: Boolean(global.payment.receipt), isReceiptModalOpen: Boolean(global.payment.receipt),
shouldSuggestStickers: global.settings.byKey.shouldSuggestStickers, shouldSuggestStickers: global.settings.byKey.shouldSuggestStickers,
recentEmojis: global.recentEmojis,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, [ (setGlobal, actions): DispatchProps => pick(actions, [

View File

@ -16,7 +16,6 @@
line-height: inherit; line-height: inherit;
} }
&.focus,
&:hover { &:hover {
background-color: rgba(var(--color-text-secondary-rgb), 0.08); background-color: rgba(var(--color-text-secondary-rgb), 0.08);
} }

View File

@ -6,20 +6,19 @@ import './EmojiButton.scss';
type OwnProps = { type OwnProps = {
emoji: Emoji; emoji: Emoji;
focus?: boolean;
onClick: (emoji: string, name: string) => void; onClick: (emoji: string, name: string) => void;
}; };
const EmojiButton: FC<OwnProps> = ({ emoji, focus, onClick }) => { const EmojiButton: FC<OwnProps> = ({ emoji, onClick }) => {
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
onClick(emoji.native, emoji.id); onClick(emoji.native, emoji.id);
}, [emoji, onClick]); }, [emoji, onClick]);
return ( return (
<div <div
className={`EmojiButton ${focus ? 'focus' : ''}`} className="EmojiButton"
onClick={handleClick} onClick={handleClick}
title={`:${emoji.names[0]}:`} title={emoji.colons}
> >
{IS_EMOJI_SUPPORTED ? emoji.native : <img src={`/img-apple-64/${emoji.image}.png`} alt="" loading="lazy" />} {IS_EMOJI_SUPPORTED ? emoji.native : <img src={`/img-apple-64/${emoji.image}.png`} alt="" loading="lazy" />}
</div> </div>

View File

@ -1,11 +1,37 @@
.EmojiTooltip { .EmojiTooltip {
display: flex; position: absolute;
padding-left: .25rem; bottom: calc(100% + .5rem);
overflow-x: auto; left: 0;
overflow-x: overlay; width: 100%;
overflow-y: hidden; background: var(--color-background);
border-radius: var(--border-radius-messages);
padding: 0.5rem 0;
max-height: 15rem;
overflow-x: hidden;
overflow-y: auto;
.EmojiButton { display: grid;
flex: 0 0 2.5rem 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;
} }
} }

View File

@ -1,110 +1,55 @@
import React, { import React, {
FC, memo, useCallback, useEffect, useRef, useState, FC, memo, useEffect, useRef,
} from '../../../lib/teact/teact'; } 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 { IS_TOUCH_ENV } from '../../../util/environment';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import cycleRestrict from '../../../util/cycleRestrict'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
import captureKeyboardListeners from '../../../util/captureKeyboardListeners'; import { pick } from '../../../util/iteratees';
import findInViewport from '../../../util/findInViewport'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import isFullyVisible from '../../../util/isFullyVisible';
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import usePrevious from '../../../hooks/usePrevious';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
import EmojiButton from './EmojiButton'; import StickerButton from '../../common/StickerButton';
import './EmojiTooltip.scss'; import './EmojiTooltip.scss';
const VIEWPORT_MARGIN = 8;
const EMOJI_BUTTON_WIDTH = 44;
function setItemVisible(index: number, containerRef: Record<string, any>) {
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 = { export type OwnProps = {
isOpen: boolean; isOpen: boolean;
onEmojiSelect: (text: string) => void; onStickerSelect: (sticker: ApiSticker) => void;
onClose: NoneToVoidFunction;
emojis: Emoji[];
}; };
const EmojiTooltip: FC<OwnProps> = ({ type StateProps = {
stickers?: ApiSticker[];
};
type DispatchProps = Pick<GlobalActions, 'clearStickersForEmoji'>;
const INTERSECTION_THROTTLE = 200;
const EmojiTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
isOpen, isOpen,
emojis, onStickerSelect,
onClose, stickers,
onEmojiSelect, clearStickersForEmoji,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); 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(() => { useEffect(() => (isOpen ? captureEscKeyListener(clearStickersForEmoji) : undefined), [isOpen, clearStickersForEmoji]);
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]);
const handleMouseEnter = () => { const handleMouseEnter = () => {
document.body.classList.add('no-select'); document.body.classList.add('no-select');
@ -115,7 +60,7 @@ const EmojiTooltip: FC<OwnProps> = ({
}; };
const className = buildClassName( const className = buildClassName(
'EmojiTooltip composer-tooltip custom-scroll-x', 'EmojiTooltip custom-scroll',
transitionClassNames, transitionClassNames,
); );
@ -126,13 +71,15 @@ const EmojiTooltip: FC<OwnProps> = ({
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined} onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined} onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
> >
{shouldRender && emojis ? ( {shouldRender && displayedStickers ? (
emojis.map((emoji, index) => ( displayedStickers.map((sticker) => (
<EmojiButton <StickerButton
key={emoji.id} key={sticker.id}
emoji={emoji} sticker={sticker}
focus={selectedIndex === index} size={STICKER_SIZE_PICKER}
onClick={onEmojiSelect} observeIntersection={observeIntersection}
onClick={onStickerSelect}
clickArg={sticker}
/> />
)) ))
) : shouldRender ? ( ) : shouldRender ? (
@ -142,4 +89,11 @@ const EmojiTooltip: FC<OwnProps> = ({
); );
}; };
export default memo(EmojiTooltip); export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { stickers } = global.stickers.forEmoji;
return { stickers };
},
(setGlobal, actions): DispatchProps => pick(actions, ['clearStickersForEmoji']),
)(EmojiTooltip));

View File

@ -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<OwnProps> = (props) => {
const { isOpen } = props;
const MentionMenu = useModuleLoader(Bundles.Extra, 'MentionMenu', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return MentionMenu ? <MentionMenu {...props} /> : undefined;
};
export default memo(MentionMenuAsync);

View File

@ -1,14 +1,41 @@
.MentionTooltip { .MentionMenu {
position: absolute;
bottom: calc(100% + .75rem);
left: 0;
width: calc(100% - 4rem); width: calc(100% - 4rem);
max-width: 20rem; 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; 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) { @media (max-width: 600px) {
width: calc(100% - 3rem); width: calc(100% - 3rem);
} }
&:not(.shown) {
display: none;
}
&.open {
opacity: 1;
transform: translateY(0);
}
.Loading {
margin: 1rem 0;
}
.ListItem.chat-item-clickable { .ListItem.chat-item-clickable {
margin: 0; margin: 0;

View File

@ -16,7 +16,7 @@ import cycleRestrict from '../../../util/cycleRestrict';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import PrivateChatInfo from '../../common/PrivateChatInfo'; import PrivateChatInfo from '../../common/PrivateChatInfo';
import './MentionTooltip.scss'; import './MentionMenu.scss';
const VIEWPORT_MARGIN = 8; const VIEWPORT_MARGIN = 8;
const SCROLL_MARGIN = 10; const SCROLL_MARGIN = 10;
@ -53,7 +53,7 @@ export type OwnProps = {
usersById?: Record<number, ApiUser>; usersById?: Record<number, ApiUser>;
}; };
const MentionTooltip: FC<OwnProps> = ({ const MentionMenu: FC<OwnProps> = ({
isOpen, isOpen,
filter, filter,
onClose, onClose,
@ -136,7 +136,7 @@ const MentionTooltip: FC<OwnProps> = ({
} }
const className = buildClassName( const className = buildClassName(
'MentionTooltip composer-tooltip custom-scroll', 'MentionMenu custom-scroll',
transitionClassNames, transitionClassNames,
); );
@ -160,4 +160,4 @@ const MentionTooltip: FC<OwnProps> = ({
); );
}; };
export default memo(MentionTooltip); export default memo(MentionMenu);

View File

@ -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<OwnProps> = (props) => {
const { isOpen } = props;
const MentionTooltip = useModuleLoader(Bundles.Extra, 'MentionTooltip', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return MentionTooltip ? <MentionTooltip {...props} /> : undefined;
};
export default memo(MentionTooltipAsync);

View File

@ -36,7 +36,6 @@ type OwnProps = {
placeholder: string; placeholder: string;
shouldSetFocus: boolean; shouldSetFocus: boolean;
shouldSupressFocus?: boolean; shouldSupressFocus?: boolean;
shouldSupressTextFormatter?: boolean;
onUpdate: (html: string) => void; onUpdate: (html: string) => void;
onSupressedFocus?: () => void; onSupressedFocus?: () => void;
onSend: () => void; onSend: () => void;
@ -77,7 +76,6 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
placeholder, placeholder,
shouldSetFocus, shouldSetFocus,
shouldSupressFocus, shouldSupressFocus,
shouldSupressTextFormatter,
onUpdate, onUpdate,
onSupressedFocus, onSupressedFocus,
onSend, onSend,
@ -141,8 +139,7 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
const selectionRange = selection.getRangeAt(0); const selectionRange = selection.getRangeAt(0);
const selectedText = selectionRange.toString().trim(); const selectedText = selectionRange.toString().trim();
if ( if (
shouldSupressTextFormatter !isSelectionInsideInput(selectionRange)
|| !isSelectionInsideInput(selectionRange)
|| !selectedText || !selectedText
|| parseEmojiOnlyString(selectedText) || parseEmojiOnlyString(selectedText)
|| !selectionRange.START_TO_END || !selectionRange.START_TO_END

View File

@ -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<OwnProps> = (props) => {
const { isOpen } = props;
const StickerTooltip = useModuleLoader(Bundles.Extra, 'StickerTooltip', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return StickerTooltip ? <StickerTooltip {...props} /> : undefined;
};
export default StickerTooltipAsync;

View File

@ -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;
}
}

View File

@ -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<GlobalActions, 'clearStickersForEmoji'>;
const INTERSECTION_THROTTLE = 200;
const StickerTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
isOpen,
onStickerSelect,
stickers,
clearStickersForEmoji,
}) => {
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(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 (
<div
ref={containerRef}
className={className}
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
>
{shouldRender && displayedStickers ? (
displayedStickers.map((sticker) => (
<StickerButton
key={sticker.id}
sticker={sticker}
size={STICKER_SIZE_PICKER}
observeIntersection={observeIntersection}
onClick={onStickerSelect}
clickArg={sticker}
/>
))
) : shouldRender ? (
<Loading />
) : undefined}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { stickers } = global.stickers.forEmoji;
return { stickers };
},
(setGlobal, actions): DispatchProps => pick(actions, ['clearStickersForEmoji']),
)(StickerTooltip));

View File

@ -1,132 +1,36 @@
import { import { useEffect } from '../../../../lib/teact/teact';
useCallback, useEffect, useMemo, useState, import { getDispatch } from '../../../../lib/teact/teactn';
} from '../../../../lib/teact/teact';
import { EDITABLE_INPUT_ID } from '../../../../config'; import { ApiSticker } from '../../../../api/types';
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';
let emojiDataPromise: Promise<EmojiModule>; import { IS_EMOJI_SUPPORTED } from '../../../../util/environment';
let emojiRawData: EmojiRawData;
let emojiData: EmojiData;
const RE_NOT_EMOJI_SEARCH = /[^-:_a-z\d]+/i; import parseEmojiOnlyString from '../../../common/helpers/parseEmojiOnlyString';
const EMOJIS_LIMIT = 50;
export default function useEmojiTooltip( export default function useEmojiTooltip(
isAllowed: boolean, isAllowed: boolean,
html: string, html: string,
recentEmojiIds: string[], stickers?: ApiSticker[],
inputId = EDITABLE_INPUT_ID,
onUpdateHtml: (html: string) => void,
) { ) {
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); const { loadStickersForEmoji, clearStickersForEmoji } = getDispatch();
const [emojis, setEmojis] = useState<Emoji[]>([]); const isSingleEmoji = (
const [filteredEmojis, setFilteredEmojis] = useState<Emoji[]>([]); (IS_EMOJI_SUPPORTED && parseEmojiOnlyString(html) === 1)
|| (!IS_EMOJI_SUPPORTED && Boolean(html.match(/^<img.[^>]*?>$/g)))
const recentEmojis = useMemo(
() => {
if (!emojis && !recentEmojiIds.length) {
return [];
}
return emojis.filter((emoji) => recentEmojiIds.includes(emoji.id)) as Emoji[];
},
[emojis, recentEmojiIds],
); );
const hasStickers = Boolean(stickers) && isSingleEmoji;
// Initialize data on first render.
useEffect(() => {
const exec = () => {
setEmojis(Object.values(emojiData.emojis));
};
if (emojiData) {
exec();
} else {
ensureEmojiData()
.then(exec);
}
}, []);
useEffect(() => { useEffect(() => {
if (!html || !emojis) { if (isAllowed && isSingleEmoji) {
unmarkIsOpen(); loadStickersForEmoji({ emoji: html });
return; } else if (hasStickers || !isSingleEmoji) {
clearStickersForEmoji();
} }
// We omit `hasStickers` here to prevent re-fetching after manually closing tooltip (via <Esc>).
const code = getEmojiCode(html); // eslint-disable-next-line react-hooks/exhaustive-deps
if (!code) { }, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed]);
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]);
return { return {
isEmojiTooltipOpen: isOpen, isEmojiTooltipOpen: hasStickers,
closeEmojiTooltip: unmarkIsOpen, closeEmojiTooltip: clearStickersForEmoji,
filteredEmojis,
insertEmoji,
}; };
} }
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<EmojiModule>;
emojiRawData = (await emojiDataPromise).default;
emojiData = uncompressEmoji(emojiRawData);
}
return emojiDataPromise;
}

View File

@ -10,7 +10,7 @@ import useFlag from '../../../../hooks/useFlag';
const RE_NOT_USERNAME_SEARCH = /[^@_\d\wа-яё]+/i; const RE_NOT_USERNAME_SEARCH = /[^@_\d\wа-яё]+/i;
export default function useMentionTooltip( export default function useMentionMenu(
canSuggestMembers: boolean | undefined, canSuggestMembers: boolean | undefined,
html: string, html: string,
onUpdateHtml: (html: string) => void, onUpdateHtml: (html: string) => void,
@ -90,9 +90,9 @@ export default function useMentionTooltip(
}, [html, inputId, onUpdateHtml, unmarkIsOpen]); }, [html, inputId, onUpdateHtml, unmarkIsOpen]);
return { return {
isMentionTooltipOpen: isOpen, isMentionMenuOpen: isOpen,
mentionFilter: currentFilter, mentionFilter: currentFilter,
closeMentionTooltip: unmarkIsOpen, closeMentionMenu: unmarkIsOpen,
insertMention, insertMention,
mentionFilteredMembers: filteredMembers, mentionFilteredMembers: filteredMembers,
}; };

View File

@ -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(/^<img.[^>]*?>$/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 <Esc>).
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed]);
return {
isStickerTooltipOpen: hasStickers,
closeStickerTooltip: clearStickersForEmoji,
};
}

View File

@ -1,5 +1,4 @@
type HandlerName = 'onEnter' | 'onBackspace' | 'onDelete' | 'onEsc' | 'onUp' | 'onDown' | 'onLeft' | 'onRight' type HandlerName = 'onEnter' | 'onBackspace' | 'onDelete' | 'onEsc' | 'onUp' | 'onDown' | 'onTab';
| 'onTab';
type Handler = (e: KeyboardEvent) => void; type Handler = (e: KeyboardEvent) => void;
type CaptureOptions = Partial<Record<HandlerName, Handler>>; type CaptureOptions = Partial<Record<HandlerName, Handler>>;
@ -11,8 +10,6 @@ const keyToHandlerName: Record<string, HandlerName> = {
Escape: 'onEsc', Escape: 'onEsc',
ArrowUp: 'onUp', ArrowUp: 'onUp',
ArrowDown: 'onDown', ArrowDown: 'onDown',
ArrowLeft: 'onLeft',
ArrowRight: 'onRight',
Tab: 'onTab', Tab: 'onTab',
}; };
@ -23,8 +20,6 @@ const handlers: Record<HandlerName, Handler[]> = {
onEsc: [], onEsc: [],
onUp: [], onUp: [],
onDown: [], onDown: [],
onLeft: [],
onRight: [],
onTab: [], onTab: [],
}; };

View File

@ -55,13 +55,13 @@ export function uncompressEmoji(data: EmojiRawData): EmojiData {
for (let j = 0; j < data[i + 1].length; j++) { for (let j = 0; j < data[i + 1].length; j++) {
const emojiRaw = data[i + 1][j]; const emojiRaw = data[i + 1][j];
if (!EXCLUDE_EMOJIS.includes(emojiRaw[1][0])) { if (!EXCLUDE_EMOJIS.includes(emojiRaw[1])) {
category.emojis.push(emojiRaw[1][0]); category.emojis.push(emojiRaw[1]);
emojiData.emojis[emojiRaw[1][0]] = { emojiData.emojis[emojiRaw[1]] = {
id: emojiRaw[1][0], id: emojiRaw[1],
names: emojiRaw[1] as string[], colons: `:${emojiRaw[1]}:`,
native: unifiedToNative(emojiRaw[0] as string), native: unifiedToNative(emojiRaw[0]),
image: (emojiRaw[0] as string).toLowerCase(), image: emojiRaw[0].toLowerCase(),
}; };
} }
} }

View File

@ -4,7 +4,7 @@ import { IS_IOS } from './environment';
const DURATION = 450; 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 // Native way seems to be smoother in Chrome
if (!IS_IOS) { if (!IS_IOS) {
container.scrollTo({ left, behavior: 'smooth' }); container.scrollTo({ left, behavior: 'smooth' });

View File

@ -4,10 +4,9 @@ export default function findInViewport(
margin = 0, margin = 0,
isDense = false, isDense = false,
shouldContainBottom = false, shouldContainBottom = false,
isHorizontal = false,
) { ) {
const viewportY1 = container[isHorizontal ? 'scrollLeft' : 'scrollTop']; const viewportY1 = container.scrollTop;
const viewportY2 = viewportY1 + container[isHorizontal ? 'offsetWidth' : 'offsetHeight']; const viewportY2 = viewportY1 + container.offsetHeight;
const allElements = typeof selectorOrElements === 'string' const allElements = typeof selectorOrElements === 'string'
? container.querySelectorAll<HTMLElement>(selectorOrElements) ? container.querySelectorAll<HTMLElement>(selectorOrElements)
: selectorOrElements; : selectorOrElements;
@ -17,8 +16,8 @@ export default function findInViewport(
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const element = allElements[i]; const element = allElements[i];
const y1 = element[isHorizontal ? 'offsetLeft' : 'offsetTop']; const y1 = element.offsetTop;
const y2 = y1 + element[isHorizontal ? 'offsetWidth' : 'offsetHeight']; const y2 = y1 + element.offsetHeight;
const isVisible = shouldContainBottom const isVisible = shouldContainBottom
? y2 >= viewportY1 - margin && y2 <= viewportY2 + margin ? y2 >= viewportY1 - margin && y2 <= viewportY2 + margin
: y1 <= viewportY2 + margin && y2 >= viewportY1 - margin; : y1 <= viewportY2 + margin && y2 >= viewportY1 - margin;

View File

@ -1,8 +1,8 @@
function isFullyVisible(container: HTMLElement, element: HTMLElement, isHorizontal = false) { function isFullyVisible(container: HTMLElement, element: HTMLElement) {
const viewportY1 = container[isHorizontal ? 'scrollLeft' : 'scrollTop']; const viewportY1 = container.scrollTop;
const viewportY2 = viewportY1 + container[isHorizontal ? 'offsetWidth' : 'offsetHeight']; const viewportY2 = viewportY1 + container.offsetHeight;
const y1 = element[isHorizontal ? 'offsetLeft' : 'offsetTop']; const y1 = element.offsetTop;
const y2 = y1 + element[isHorizontal ? 'offsetWidth' : 'offsetHeight']; const y2 = y1 + element.offsetHeight;
return y1 > viewportY1 && y2 < viewportY2; return y1 > viewportY1 && y2 < viewportY2;
} }