Composer: Add Emoji Picker (#1061)
This commit is contained in:
parent
59b848697c
commit
4559e0ff8f
4
package-lock.json
generated
4
package-lock.json
generated
@ -7660,8 +7660,8 @@
|
||||
"dev": true
|
||||
},
|
||||
"emoji-data-ios": {
|
||||
"version": "github:korenskoy/emoji-data-ios#10073b1244de618a3e587ae1d91b5e46ec01fd06",
|
||||
"from": "github:korenskoy/emoji-data-ios#10073b1"
|
||||
"version": "github:korenskoy/emoji-data-ios#e2c6557d2d36612a882d9b81b2467f441f1f4179",
|
||||
"from": "github:korenskoy/emoji-data-ios#e2c6557"
|
||||
},
|
||||
"emojis-list": {
|
||||
"version": "2.1.0",
|
||||
|
||||
@ -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#10073b1",
|
||||
"emoji-data-ios": "github:korenskoy/emoji-data-ios#e2c6557",
|
||||
"events": "^3.0.0",
|
||||
"idb-keyval": "^5.0.5",
|
||||
"opus-recorder": "^6.2.0",
|
||||
|
||||
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
@ -38,7 +38,7 @@ type EmojiCategory = {
|
||||
|
||||
type Emoji = {
|
||||
id: string;
|
||||
colons: string;
|
||||
names: string[];
|
||||
native: string;
|
||||
image: string;
|
||||
skin?: number;
|
||||
|
||||
@ -27,12 +27,13 @@ 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 MentionMenu } from '../components/middle/composer/MentionMenu';
|
||||
export { default as EmojiTooltip } from '../components/middle/composer/EmojiTooltip';
|
||||
export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip';
|
||||
export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip';
|
||||
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';
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
.MentionMenu {
|
||||
.MentionTooltip {
|
||||
right: 0 !important;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@ -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 useMentionMenu from './hooks/useMentionMenu';
|
||||
import useMentionTooltip from './hooks/useMentionTooltip';
|
||||
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 MentionMenu from './MentionMenu';
|
||||
import MentionTooltip from './MentionTooltip';
|
||||
|
||||
import './AttachmentModal.scss';
|
||||
|
||||
@ -47,10 +47,10 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
const isOpen = Boolean(attachments.length);
|
||||
|
||||
const {
|
||||
isMentionMenuOpen, mentionFilter,
|
||||
closeMentionMenu, insertMention,
|
||||
isMentionTooltipOpen, mentionFilter,
|
||||
closeMentionTooltip, insertMention,
|
||||
mentionFilteredMembers,
|
||||
} = useMentionMenu(
|
||||
} = useMentionTooltip(
|
||||
canSuggestMembers && isOpen,
|
||||
caption,
|
||||
onCaptionUpdate,
|
||||
@ -136,9 +136,9 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
)}
|
||||
|
||||
<div className="attachment-caption-wrapper">
|
||||
<MentionMenu
|
||||
isOpen={isMentionMenuOpen}
|
||||
onClose={closeMentionMenu}
|
||||
<MentionTooltip
|
||||
isOpen={isMentionTooltipOpen}
|
||||
onClose={closeMentionTooltip}
|
||||
filter={mentionFilter}
|
||||
onInsertUserName={insertMention}
|
||||
filteredChatMembers={mentionFilteredMembers}
|
||||
|
||||
@ -368,3 +368,37 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,8 +55,9 @@ import useClipboardPaste from './hooks/useClipboardPaste';
|
||||
import useDraft from './hooks/useDraft';
|
||||
import useEditing from './hooks/useEditing';
|
||||
import usePrevious from '../../../hooks/usePrevious';
|
||||
import useStickerTooltip from './hooks/useStickerTooltip';
|
||||
import useEmojiTooltip from './hooks/useEmojiTooltip';
|
||||
import useMentionMenu from './hooks/useMentionMenu';
|
||||
import useMentionTooltip from './hooks/useMentionTooltip';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
@ -66,8 +67,9 @@ import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
import AttachMenu from './AttachMenu.async';
|
||||
import SymbolMenu from './SymbolMenu.async';
|
||||
import MentionMenu from './MentionMenu.async';
|
||||
import MentionTooltip from './MentionTooltip.async';
|
||||
import CustomSendMenu from './CustomSendMenu.async';
|
||||
import StickerTooltip from './StickerTooltip.async';
|
||||
import EmojiTooltip from './EmojiTooltip.async';
|
||||
import BotKeyboardMenu from './BotKeyboardMenu.async';
|
||||
import MessageInput from './MessageInput';
|
||||
@ -112,6 +114,7 @@ type StateProps = {
|
||||
groupChatMembers?: ApiChatMember[];
|
||||
currentUserId?: number;
|
||||
usersById?: Record<number, ApiUser>;
|
||||
recentEmojis: string[];
|
||||
lastSyncTime?: number;
|
||||
contentToBeScheduled?: GlobalState['messages']['contentToBeScheduled'];
|
||||
shouldSuggestStickers?: boolean;
|
||||
@ -171,6 +174,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
lastSyncTime,
|
||||
contentToBeScheduled,
|
||||
shouldSuggestStickers,
|
||||
recentEmojis,
|
||||
sendMessage,
|
||||
editMessage,
|
||||
saveDraft,
|
||||
@ -254,10 +258,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const canShowCustomSendMenu = !shouldSchedule;
|
||||
|
||||
const {
|
||||
isMentionMenuOpen, mentionFilter,
|
||||
closeMentionMenu, insertMention,
|
||||
isMentionTooltipOpen, mentionFilter,
|
||||
closeMentionTooltip, insertMention,
|
||||
mentionFilteredMembers,
|
||||
} = useMentionMenu(
|
||||
} = useMentionTooltip(
|
||||
canSuggestMembers && !attachments.length,
|
||||
html,
|
||||
setHtml,
|
||||
@ -281,11 +285,20 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const isAdmin = chat && isChatAdmin(chat);
|
||||
const slowMode = getChatSlowModeOptions(chat);
|
||||
|
||||
const { isEmojiTooltipOpen, closeEmojiTooltip } = useEmojiTooltip(
|
||||
const { isStickerTooltipOpen, closeStickerTooltip } = useStickerTooltip(
|
||||
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()!;
|
||||
@ -337,10 +350,11 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const resetComposer = useCallback(() => {
|
||||
setHtml('');
|
||||
setAttachments([]);
|
||||
closeEmojiTooltip();
|
||||
closeStickerTooltip();
|
||||
closeCalendar();
|
||||
setScheduledMessageArgs(undefined);
|
||||
closeMentionMenu();
|
||||
closeMentionTooltip();
|
||||
closeEmojiTooltip();
|
||||
|
||||
if (IS_MOBILE_SCREEN) {
|
||||
// @perf
|
||||
@ -348,7 +362,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
} else {
|
||||
closeSymbolMenu();
|
||||
}
|
||||
}, [closeEmojiTooltip, closeCalendar, closeMentionMenu, closeSymbolMenu]);
|
||||
}, [closeStickerTooltip, closeCalendar, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu]);
|
||||
|
||||
// Handle chat change
|
||||
const prevChatId = usePrevious(chatId);
|
||||
@ -689,10 +703,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
message={renderedEditedMessage}
|
||||
/>
|
||||
)}
|
||||
<MentionMenu
|
||||
isOpen={isMentionMenuOpen}
|
||||
<MentionTooltip
|
||||
isOpen={isMentionTooltipOpen}
|
||||
filter={mentionFilter}
|
||||
onClose={closeMentionMenu}
|
||||
onClose={closeMentionTooltip}
|
||||
onInsertUserName={insertMention}
|
||||
filteredChatMembers={mentionFilteredMembers}
|
||||
usersById={usersById}
|
||||
@ -739,6 +753,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
}
|
||||
shouldSetFocus={isSymbolMenuOpen}
|
||||
shouldSupressFocus={IS_MOBILE_SCREEN && isSymbolMenuOpen}
|
||||
shouldSupressTextFormatter={isEmojiTooltipOpen || isMentionTooltipOpen}
|
||||
onUpdate={setHtml}
|
||||
onSend={mainButtonState === MainButtonState.Edit
|
||||
? handleEditComplete
|
||||
@ -786,9 +801,15 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
{formatVoiceRecordDuration(currentRecordTime - startRecordTimeRef.current!)}
|
||||
</span>
|
||||
)}
|
||||
<StickerTooltip
|
||||
isOpen={isStickerTooltipOpen}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
/>
|
||||
<EmojiTooltip
|
||||
isOpen={isEmojiTooltipOpen}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
emojis={filteredEmojis}
|
||||
onClose={closeEmojiTooltip}
|
||||
onEmojiSelect={insertEmoji}
|
||||
/>
|
||||
<AttachMenu
|
||||
isOpen={isAttachMenuOpen}
|
||||
@ -909,6 +930,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isPaymentModalOpen: global.payment.isPaymentModalOpen,
|
||||
isReceiptModalOpen: Boolean(global.payment.receipt),
|
||||
shouldSuggestStickers: global.settings.byKey.shouldSuggestStickers,
|
||||
recentEmojis: global.recentEmojis,
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
&.focus,
|
||||
&:hover {
|
||||
background-color: rgba(var(--color-text-secondary-rgb), 0.08);
|
||||
}
|
||||
|
||||
@ -6,19 +6,20 @@ import './EmojiButton.scss';
|
||||
|
||||
type OwnProps = {
|
||||
emoji: Emoji;
|
||||
focus?: boolean;
|
||||
onClick: (emoji: string, name: string) => void;
|
||||
};
|
||||
|
||||
const EmojiButton: FC<OwnProps> = ({ emoji, onClick }) => {
|
||||
const EmojiButton: FC<OwnProps> = ({ emoji, focus, onClick }) => {
|
||||
const handleClick = useCallback(() => {
|
||||
onClick(emoji.native, emoji.id);
|
||||
}, [emoji, onClick]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="EmojiButton"
|
||||
className={`EmojiButton ${focus ? 'focus' : ''}`}
|
||||
onClick={handleClick}
|
||||
title={emoji.colons}
|
||||
title={`:${emoji.names[0]}:`}
|
||||
>
|
||||
{IS_EMOJI_SUPPORTED ? emoji.native : <img src={`/img-apple-64/${emoji.image}.png`} alt="" loading="lazy" />}
|
||||
</div>
|
||||
|
||||
@ -1,37 +1,11 @@
|
||||
.EmojiTooltip {
|
||||
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;
|
||||
display: flex;
|
||||
padding-left: .25rem;
|
||||
overflow-x: auto;
|
||||
overflow-x: overlay;
|
||||
overflow-y: hidden;
|
||||
|
||||
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;
|
||||
.EmojiButton {
|
||||
flex: 0 0 2.5rem
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,55 +1,110 @@
|
||||
import React, {
|
||||
FC, memo, useEffect, useRef,
|
||||
FC, memo, useCallback, useEffect, useRef, useState,
|
||||
} 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 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 useShowTransition from '../../../hooks/useShowTransition';
|
||||
import usePrevious from '../../../hooks/usePrevious';
|
||||
|
||||
import Loading from '../../ui/Loading';
|
||||
import StickerButton from '../../common/StickerButton';
|
||||
import EmojiButton from './EmojiButton';
|
||||
|
||||
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 = {
|
||||
isOpen: boolean;
|
||||
onStickerSelect: (sticker: ApiSticker) => void;
|
||||
onEmojiSelect: (text: string) => void;
|
||||
onClose: NoneToVoidFunction;
|
||||
emojis: Emoji[];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
stickers?: ApiSticker[];
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'clearStickersForEmoji'>;
|
||||
|
||||
const INTERSECTION_THROTTLE = 200;
|
||||
|
||||
const EmojiTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const EmojiTooltip: FC<OwnProps> = ({
|
||||
isOpen,
|
||||
onStickerSelect,
|
||||
stickers,
|
||||
clearStickersForEmoji,
|
||||
emojis,
|
||||
onClose,
|
||||
onEmojiSelect,
|
||||
}) => {
|
||||
// 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 });
|
||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||
|
||||
useEffect(() => (isOpen ? captureEscKeyListener(clearStickersForEmoji) : undefined), [isOpen, clearStickersForEmoji]);
|
||||
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]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
document.body.classList.add('no-select');
|
||||
@ -60,7 +115,7 @@ const EmojiTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
};
|
||||
|
||||
const className = buildClassName(
|
||||
'EmojiTooltip custom-scroll',
|
||||
'EmojiTooltip composer-tooltip custom-scroll-x',
|
||||
transitionClassNames,
|
||||
);
|
||||
|
||||
@ -71,15 +126,13 @@ const EmojiTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
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 && emojis ? (
|
||||
emojis.map((emoji, index) => (
|
||||
<EmojiButton
|
||||
key={emoji.id}
|
||||
emoji={emoji}
|
||||
focus={selectedIndex === index}
|
||||
onClick={onEmojiSelect}
|
||||
/>
|
||||
))
|
||||
) : shouldRender ? (
|
||||
@ -89,11 +142,4 @@ const EmojiTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const { stickers } = global.stickers.forEmoji;
|
||||
|
||||
return { stickers };
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, ['clearStickersForEmoji']),
|
||||
)(EmojiTooltip));
|
||||
export default memo(EmojiTooltip);
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
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);
|
||||
15
src/components/middle/composer/MentionTooltip.async.tsx
Normal file
15
src/components/middle/composer/MentionTooltip.async.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
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);
|
||||
@ -1,41 +1,14 @@
|
||||
.MentionMenu {
|
||||
position: absolute;
|
||||
bottom: calc(100% + .75rem);
|
||||
left: 0;
|
||||
.MentionTooltip {
|
||||
width: calc(100% - 4rem);
|
||||
max-width: 20rem;
|
||||
background: var(--color-background);
|
||||
border-radius: var(--border-radius-messages);
|
||||
padding: 0.5rem 0;
|
||||
max-height: 15rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
flex-direction: column;
|
||||
|
||||
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;
|
||||
|
||||
@ -16,7 +16,7 @@ import cycleRestrict from '../../../util/cycleRestrict';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
|
||||
import './MentionMenu.scss';
|
||||
import './MentionTooltip.scss';
|
||||
|
||||
const VIEWPORT_MARGIN = 8;
|
||||
const SCROLL_MARGIN = 10;
|
||||
@ -53,7 +53,7 @@ export type OwnProps = {
|
||||
usersById?: Record<number, ApiUser>;
|
||||
};
|
||||
|
||||
const MentionMenu: FC<OwnProps> = ({
|
||||
const MentionTooltip: FC<OwnProps> = ({
|
||||
isOpen,
|
||||
filter,
|
||||
onClose,
|
||||
@ -136,7 +136,7 @@ const MentionMenu: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
const className = buildClassName(
|
||||
'MentionMenu custom-scroll',
|
||||
'MentionTooltip composer-tooltip custom-scroll',
|
||||
transitionClassNames,
|
||||
);
|
||||
|
||||
@ -160,4 +160,4 @@ const MentionMenu: FC<OwnProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MentionMenu);
|
||||
export default memo(MentionTooltip);
|
||||
@ -36,6 +36,7 @@ type OwnProps = {
|
||||
placeholder: string;
|
||||
shouldSetFocus: boolean;
|
||||
shouldSupressFocus?: boolean;
|
||||
shouldSupressTextFormatter?: boolean;
|
||||
onUpdate: (html: string) => void;
|
||||
onSupressedFocus?: () => void;
|
||||
onSend: () => void;
|
||||
@ -76,6 +77,7 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
placeholder,
|
||||
shouldSetFocus,
|
||||
shouldSupressFocus,
|
||||
shouldSupressTextFormatter,
|
||||
onUpdate,
|
||||
onSupressedFocus,
|
||||
onSend,
|
||||
@ -139,7 +141,8 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const selectionRange = selection.getRangeAt(0);
|
||||
const selectedText = selectionRange.toString().trim();
|
||||
if (
|
||||
!isSelectionInsideInput(selectionRange)
|
||||
shouldSupressTextFormatter
|
||||
|| !isSelectionInsideInput(selectionRange)
|
||||
|| !selectedText
|
||||
|| parseEmojiOnlyString(selectedText)
|
||||
|| !selectionRange.START_TO_END
|
||||
|
||||
15
src/components/middle/composer/StickerTooltip.async.tsx
Normal file
15
src/components/middle/composer/StickerTooltip.async.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
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;
|
||||
10
src/components/middle/composer/StickerTooltip.scss
Normal file
10
src/components/middle/composer/StickerTooltip.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.StickerTooltip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(5rem, 1fr));
|
||||
grid-auto-rows: auto;
|
||||
place-items: center;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
100
src/components/middle/composer/StickerTooltip.tsx
Normal file
100
src/components/middle/composer/StickerTooltip.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
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));
|
||||
@ -1,36 +1,132 @@
|
||||
import { useEffect } from '../../../../lib/teact/teact';
|
||||
import { getDispatch } from '../../../../lib/teact/teactn';
|
||||
import {
|
||||
useCallback, useEffect, useMemo, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
|
||||
import { ApiSticker } from '../../../../api/types';
|
||||
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 { IS_EMOJI_SUPPORTED } from '../../../../util/environment';
|
||||
let emojiDataPromise: Promise<EmojiModule>;
|
||||
let emojiRawData: EmojiRawData;
|
||||
let emojiData: EmojiData;
|
||||
|
||||
import parseEmojiOnlyString from '../../../common/helpers/parseEmojiOnlyString';
|
||||
const RE_NOT_EMOJI_SEARCH = /[^-:_a-z\d]+/i;
|
||||
const EMOJIS_LIMIT = 50;
|
||||
|
||||
export default function useEmojiTooltip(
|
||||
isAllowed: boolean,
|
||||
html: string,
|
||||
stickers?: ApiSticker[],
|
||||
recentEmojiIds: string[],
|
||||
inputId = EDITABLE_INPUT_ID,
|
||||
onUpdateHtml: (html: string) => void,
|
||||
) {
|
||||
const { loadStickersForEmoji, clearStickersForEmoji } = getDispatch();
|
||||
const isSingleEmoji = (
|
||||
(IS_EMOJI_SUPPORTED && parseEmojiOnlyString(html) === 1)
|
||||
|| (!IS_EMOJI_SUPPORTED && Boolean(html.match(/^<img.[^>]*?>$/g)))
|
||||
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
||||
const [emojis, setEmojis] = useState<Emoji[]>([]);
|
||||
const [filteredEmojis, setFilteredEmojis] = useState<Emoji[]>([]);
|
||||
|
||||
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(() => {
|
||||
if (isAllowed && isSingleEmoji) {
|
||||
loadStickersForEmoji({ emoji: html });
|
||||
} else if (hasStickers || !isSingleEmoji) {
|
||||
clearStickersForEmoji();
|
||||
if (!html || !emojis) {
|
||||
unmarkIsOpen();
|
||||
return;
|
||||
}
|
||||
// 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]);
|
||||
|
||||
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]);
|
||||
|
||||
return {
|
||||
isEmojiTooltipOpen: hasStickers,
|
||||
closeEmojiTooltip: clearStickersForEmoji,
|
||||
isEmojiTooltipOpen: isOpen,
|
||||
closeEmojiTooltip: unmarkIsOpen,
|
||||
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;
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ import useFlag from '../../../../hooks/useFlag';
|
||||
|
||||
const RE_NOT_USERNAME_SEARCH = /[^@_\d\wа-яё]+/i;
|
||||
|
||||
export default function useMentionMenu(
|
||||
export default function useMentionTooltip(
|
||||
canSuggestMembers: boolean | undefined,
|
||||
html: string,
|
||||
onUpdateHtml: (html: string) => void,
|
||||
@ -90,9 +90,9 @@ export default function useMentionMenu(
|
||||
}, [html, inputId, onUpdateHtml, unmarkIsOpen]);
|
||||
|
||||
return {
|
||||
isMentionMenuOpen: isOpen,
|
||||
isMentionTooltipOpen: isOpen,
|
||||
mentionFilter: currentFilter,
|
||||
closeMentionMenu: unmarkIsOpen,
|
||||
closeMentionTooltip: unmarkIsOpen,
|
||||
insertMention,
|
||||
mentionFilteredMembers: filteredMembers,
|
||||
};
|
||||
36
src/components/middle/composer/hooks/useStickerTooltip.ts
Normal file
36
src/components/middle/composer/hooks/useStickerTooltip.ts
Normal file
@ -0,0 +1,36 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
type HandlerName = 'onEnter' | 'onBackspace' | 'onDelete' | 'onEsc' | 'onUp' | 'onDown' | 'onTab';
|
||||
type HandlerName = 'onEnter' | 'onBackspace' | 'onDelete' | 'onEsc' | 'onUp' | 'onDown' | 'onLeft' | 'onRight'
|
||||
| 'onTab';
|
||||
type Handler = (e: KeyboardEvent) => void;
|
||||
type CaptureOptions = Partial<Record<HandlerName, Handler>>;
|
||||
|
||||
@ -10,6 +11,8 @@ const keyToHandlerName: Record<string, HandlerName> = {
|
||||
Escape: 'onEsc',
|
||||
ArrowUp: 'onUp',
|
||||
ArrowDown: 'onDown',
|
||||
ArrowLeft: 'onLeft',
|
||||
ArrowRight: 'onRight',
|
||||
Tab: 'onTab',
|
||||
};
|
||||
|
||||
@ -20,6 +23,8 @@ const handlers: Record<HandlerName, Handler[]> = {
|
||||
onEsc: [],
|
||||
onUp: [],
|
||||
onDown: [],
|
||||
onLeft: [],
|
||||
onRight: [],
|
||||
onTab: [],
|
||||
};
|
||||
|
||||
|
||||
@ -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])) {
|
||||
category.emojis.push(emojiRaw[1]);
|
||||
emojiData.emojis[emojiRaw[1]] = {
|
||||
id: emojiRaw[1],
|
||||
colons: `:${emojiRaw[1]}:`,
|
||||
native: unifiedToNative(emojiRaw[0]),
|
||||
image: emojiRaw[0].toLowerCase(),
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { IS_IOS } from './environment';
|
||||
|
||||
const DURATION = 450;
|
||||
|
||||
export default function fastSmoothScroll(container: HTMLElement, left: number) {
|
||||
export default function fastSmoothScrollHorizontal(container: HTMLElement, left: number) {
|
||||
// Native way seems to be smoother in Chrome
|
||||
if (!IS_IOS) {
|
||||
container.scrollTo({ left, behavior: 'smooth' });
|
||||
|
||||
@ -4,9 +4,10 @@ export default function findInViewport(
|
||||
margin = 0,
|
||||
isDense = false,
|
||||
shouldContainBottom = false,
|
||||
isHorizontal = false,
|
||||
) {
|
||||
const viewportY1 = container.scrollTop;
|
||||
const viewportY2 = viewportY1 + container.offsetHeight;
|
||||
const viewportY1 = container[isHorizontal ? 'scrollLeft' : 'scrollTop'];
|
||||
const viewportY2 = viewportY1 + container[isHorizontal ? 'offsetWidth' : 'offsetHeight'];
|
||||
const allElements = typeof selectorOrElements === 'string'
|
||||
? container.querySelectorAll<HTMLElement>(selectorOrElements)
|
||||
: selectorOrElements;
|
||||
@ -16,8 +17,8 @@ export default function findInViewport(
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
const element = allElements[i];
|
||||
const y1 = element.offsetTop;
|
||||
const y2 = y1 + element.offsetHeight;
|
||||
const y1 = element[isHorizontal ? 'offsetLeft' : 'offsetTop'];
|
||||
const y2 = y1 + element[isHorizontal ? 'offsetWidth' : 'offsetHeight'];
|
||||
const isVisible = shouldContainBottom
|
||||
? y2 >= viewportY1 - margin && y2 <= viewportY2 + margin
|
||||
: y1 <= viewportY2 + margin && y2 >= viewportY1 - margin;
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
function isFullyVisible(container: HTMLElement, element: HTMLElement) {
|
||||
const viewportY1 = container.scrollTop;
|
||||
const viewportY2 = viewportY1 + container.offsetHeight;
|
||||
const y1 = element.offsetTop;
|
||||
const y2 = y1 + element.offsetHeight;
|
||||
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'];
|
||||
return y1 > viewportY1 && y2 < viewportY2;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user