Emoji Tooltip: Disable tooltip for a single character (#1178)

This commit is contained in:
Alexander Zinchuk 2021-06-18 12:19:44 +03:00
parent 824c2f684a
commit 3eb8d6259f
5 changed files with 43 additions and 20 deletions

View File

@ -33,6 +33,7 @@ export type OwnProps = {
usersById?: Record<number, ApiUser>; usersById?: Record<number, ApiUser>;
recentEmojis: string[]; recentEmojis: string[];
language: LangCode; language: LangCode;
emojiKeywords?: Record<string, string[]>;
addRecentEmoji: AnyToVoidFunction; addRecentEmoji: AnyToVoidFunction;
loadEmojiKeywords: AnyToVoidFunction; loadEmojiKeywords: AnyToVoidFunction;
onCaptionUpdate: (html: string) => void; onCaptionUpdate: (html: string) => void;
@ -52,6 +53,7 @@ const AttachmentModal: FC<OwnProps> = ({
usersById, usersById,
recentEmojis, recentEmojis,
language, language,
emojiKeywords,
onCaptionUpdate, onCaptionUpdate,
addRecentEmoji, addRecentEmoji,
loadEmojiKeywords, loadEmojiKeywords,
@ -89,6 +91,7 @@ const AttachmentModal: FC<OwnProps> = ({
recentEmojis, recentEmojis,
EDITABLE_INPUT_MODAL_ID, EDITABLE_INPUT_MODAL_ID,
onCaptionUpdate, onCaptionUpdate,
emojiKeywords,
); );
useEffect(() => (isOpen ? captureEscKeyListener(onClear) : undefined), [isOpen, onClear]); useEffect(() => (isOpen ? captureEscKeyListener(onClear) : undefined), [isOpen, onClear]);

View File

@ -673,6 +673,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
: (isSymbolMenuOpen && 'is-loading'), : (isSymbolMenuOpen && 'is-loading'),
); );
const onSend = mainButtonState === MainButtonState.Edit
? handleEditComplete
: (shouldSchedule ? openCalendar : handleSend);
return ( return (
<div className={className}> <div className={className}>
{allowedAttachmentOptions.canAttachMedia && ( {allowedAttachmentOptions.canAttachMedia && (
@ -695,6 +699,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
recentEmojis={recentEmojis} recentEmojis={recentEmojis}
onCaptionUpdate={setHtml} onCaptionUpdate={setHtml}
language={language} language={language}
emojiKeywords={emojiKeywords}
addRecentEmoji={addRecentEmoji} addRecentEmoji={addRecentEmoji}
loadEmojiKeywords={loadEmojiKeywords} loadEmojiKeywords={loadEmojiKeywords}
onSend={shouldSchedule ? openCalendar : handleSend} onSend={shouldSchedule ? openCalendar : handleSend}
@ -774,9 +779,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
shouldSupressFocus={IS_MOBILE_SCREEN && isSymbolMenuOpen} shouldSupressFocus={IS_MOBILE_SCREEN && isSymbolMenuOpen}
shouldSupressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen} shouldSupressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen}
onUpdate={setHtml} onUpdate={setHtml}
onSend={mainButtonState === MainButtonState.Edit onSend={onSend}
? handleEditComplete
: (shouldSchedule ? openCalendar : handleSend)}
onSupressedFocus={closeSymbolMenu} onSupressedFocus={closeSymbolMenu}
/> />
{withScheduledButton && ( {withScheduledButton && (

View File

@ -18,7 +18,7 @@
&.focus, &.focus,
&:hover { &:hover {
background-color: var(--color-interactive-element-hover); background-color: var(--color-background-selected);
} }
& > img { & > img {

View File

@ -2,6 +2,8 @@ import React, {
FC, memo, useCallback, useEffect, useRef, useState, FC, memo, useCallback, useEffect, useRef, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { LangCode } from '../../../types';
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 cycleRestrict from '../../../util/cycleRestrict';
@ -16,10 +18,11 @@ import Loading from '../../ui/Loading';
import EmojiButton from './EmojiButton'; import EmojiButton from './EmojiButton';
import './EmojiTooltip.scss'; import './EmojiTooltip.scss';
import { LangCode } from '../../../types';
const VIEWPORT_MARGIN = 8; const VIEWPORT_MARGIN = 8;
const EMOJI_BUTTON_WIDTH = 44; const EMOJI_BUTTON_WIDTH = 44;
const CLOSE_DURATION = 350;
const NO_EMOJI_SELECTED_INDEX = -1;
function setItemVisible(index: number, containerRef: Record<string, any>) { function setItemVisible(index: number, containerRef: Record<string, any>) {
const container = containerRef.current!; const container = containerRef.current!;
@ -59,8 +62,6 @@ export type OwnProps = {
emojis: Emoji[]; emojis: Emoji[];
}; };
const CLOSE_DURATION = 350;
const EmojiTooltip: FC<OwnProps> = ({ const EmojiTooltip: FC<OwnProps> = ({
isOpen, isOpen,
language, language,
@ -75,7 +76,7 @@ const EmojiTooltip: FC<OwnProps> = ({
const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false);
const listEmojis: Emoji[] = usePrevDuringAnimation(emojis.length ? emojis : undefined, CLOSE_DURATION) || []; const listEmojis: Emoji[] = usePrevDuringAnimation(emojis.length ? emojis : undefined, CLOSE_DURATION) || [];
const [selectedIndex, setSelectedIndex] = useState(-1); const [selectedIndex, setSelectedIndex] = useState(NO_EMOJI_SELECTED_INDEX);
useEffect(() => { useEffect(() => {
loadEmojiKeywords({ language }); loadEmojiKeywords({ language });
@ -91,21 +92,20 @@ const EmojiTooltip: FC<OwnProps> = ({
const getSelectedIndex = useCallback((newIndex: number) => { const getSelectedIndex = useCallback((newIndex: number) => {
if (!emojis.length) { if (!emojis.length) {
return -1; return NO_EMOJI_SELECTED_INDEX;
} }
const emojisCount = emojis.length; const emojisCount = emojis.length;
return cycleRestrict(emojisCount, newIndex); return cycleRestrict(emojisCount, newIndex);
}, [emojis]); }, [emojis]);
const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => { const handleArrowKey = useCallback((value: number, e: KeyboardEvent) => {
e.preventDefault(); e.preventDefault();
setSelectedIndex((index) => (getSelectedIndex(index + value))); setSelectedIndex((index) => (getSelectedIndex(index + value)));
}, [setSelectedIndex, getSelectedIndex]); }, [setSelectedIndex, getSelectedIndex]);
const handleSelectEmoji = useCallback((e: KeyboardEvent) => { const handleSelectEmoji = useCallback((e: KeyboardEvent) => {
if (emojis.length && selectedIndex > -1) { if (emojis.length && selectedIndex > NO_EMOJI_SELECTED_INDEX) {
const emoji = emojis[selectedIndex]; const emoji = emojis[selectedIndex];
if (emoji) { if (emoji) {
e.preventDefault(); e.preventDefault();

View File

@ -17,8 +17,16 @@ let emojiDataPromise: Promise<EmojiModule>;
let emojiRawData: EmojiRawData; let emojiRawData: EmojiRawData;
let emojiData: EmojiData; let emojiData: EmojiData;
const RE_NOT_EMOJI_SEARCH = /[^-_:\p{L}\p{N}]+/iu; let RE_NOT_EMOJI_SEARCH: RegExp;
const EMOJIS_LIMIT = 36; const EMOJIS_LIMIT = 36;
const FILTER_MIN_LENGTH = 2;
try {
RE_NOT_EMOJI_SEARCH = new RegExp('[^-_:\\p{L}\\p{N}]+', 'iu');
} catch (e) {
// Support for older versions of firefox
RE_NOT_EMOJI_SEARCH = new RegExp('[^-_:\\d\\wа-яё]+', 'i');
}
export default function useEmojiTooltip( export default function useEmojiTooltip(
isAllowed: boolean, isAllowed: boolean,
@ -31,7 +39,9 @@ export default function useEmojiTooltip(
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag(); const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
const [byId, setById] = useState<Record<string, Emoji> | undefined>(); const [byId, setById] = useState<Record<string, Emoji> | undefined>();
const [keywords, setKeywords] = useState<string[]>();
const [byKeyword, setByKeyword] = useState<Record<string, Emoji[]>>({}); const [byKeyword, setByKeyword] = useState<Record<string, Emoji[]>>({});
const [names, setNames] = useState<string[]>();
const [byName, setByName] = useState<Record<string, Emoji[]>>({}); const [byName, setByName] = useState<Record<string, Emoji[]>>({});
const [filteredEmojis, setFilteredEmojis] = useState<Emoji[]>([]); const [filteredEmojis, setFilteredEmojis] = useState<Emoji[]>([]);
@ -70,12 +80,14 @@ export default function useEmojiTooltip(
if (emojiKeywords) { if (emojiKeywords) {
const byNative = buildCollectionByKey(emojis, 'native'); const byNative = buildCollectionByKey(emojis, 'native');
setByKeyword(mapValues(emojiKeywords, (natives) => { const emojisByKeyword = mapValues(emojiKeywords, (natives) => {
return Object.values(pickTruthy(byNative, natives)); return Object.values(pickTruthy(byNative, natives));
})); });
setByKeyword(emojisByKeyword);
setKeywords(Object.keys(emojisByKeyword));
} }
setByName(emojis.reduce((result, emoji) => { const emojisByName = emojis.reduce((result, emoji) => {
emoji.names.forEach((name) => { emoji.names.forEach((name) => {
if (!result[name]) { if (!result[name]) {
result[name] = []; result[name] = [];
@ -85,7 +97,9 @@ export default function useEmojiTooltip(
}); });
return result; return result;
}, {} as Record<string, Emoji[]>)); }, {} as Record<string, Emoji[]>);
setByName(emojisByName);
setNames(Object.keys(emojisByName));
}, [byId, emojiKeywords]); }, [byId, emojiKeywords]);
useEffect(() => { useEffect(() => {
@ -106,12 +120,12 @@ export default function useEmojiTooltip(
if (!filter) { if (!filter) {
matched = recentEmojis; matched = recentEmojis;
} else { } else if (filter.length >= FILTER_MIN_LENGTH) {
const matchedKeywords = Object.keys(byKeyword).filter((keyword) => keyword.startsWith(filter)); const matchedKeywords = keywords.filter((keyword) => keyword.startsWith(filter)).sort();
matched = matched.concat(flatten(Object.values(pickTruthy(byKeyword, matchedKeywords)))); matched = matched.concat(flatten(Object.values(pickTruthy(byKeyword, matchedKeywords))));
// Also search by names, which is useful for non-English languages // Also search by names, which is useful for non-English languages
const matchedNames = Object.keys(byName).filter((name) => name.startsWith(filter)); const matchedNames = names.filter((name) => name.startsWith(filter));
matched = matched.concat(flatten(Object.values(pickTruthy(byName, matchedNames)))); matched = matched.concat(flatten(Object.values(pickTruthy(byName, matchedNames))));
matched = unique(matched); matched = unique(matched);
@ -123,7 +137,10 @@ export default function useEmojiTooltip(
} else { } else {
unmarkIsOpen(); unmarkIsOpen();
} }
}, [byId, byKeyword, byName, html, isAllowed, markIsOpen, recentEmojis, unmarkIsOpen]); }, [
byId, byKeyword, keywords, byName, names,
html, isAllowed, markIsOpen, recentEmojis, unmarkIsOpen,
]);
const insertEmoji = useCallback((textEmoji: string) => { const insertEmoji = useCallback((textEmoji: string) => {
const atIndex = html.lastIndexOf(':'); const atIndex = html.lastIndexOf(':');