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
|
"dev": true
|
||||||
},
|
},
|
||||||
"emoji-data-ios": {
|
"emoji-data-ios": {
|
||||||
"version": "github:korenskoy/emoji-data-ios#10073b1244de618a3e587ae1d91b5e46ec01fd06",
|
"version": "github:korenskoy/emoji-data-ios#e2c6557d2d36612a882d9b81b2467f441f1f4179",
|
||||||
"from": "github:korenskoy/emoji-data-ios#10073b1"
|
"from": "github:korenskoy/emoji-data-ios#e2c6557"
|
||||||
},
|
},
|
||||||
"emojis-list": {
|
"emojis-list": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
|
|||||||
@ -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#10073b1",
|
"emoji-data-ios": "github:korenskoy/emoji-data-ios#e2c6557",
|
||||||
"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",
|
||||||
|
|||||||
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
@ -38,7 +38,7 @@ type EmojiCategory = {
|
|||||||
|
|
||||||
type Emoji = {
|
type Emoji = {
|
||||||
id: string;
|
id: string;
|
||||||
colons: string;
|
names: string[];
|
||||||
native: string;
|
native: string;
|
||||||
image: string;
|
image: string;
|
||||||
skin?: number;
|
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 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 MentionMenu } from '../components/middle/composer/MentionMenu';
|
export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip';
|
||||||
export { default as EmojiTooltip } from '../components/middle/composer/EmojiTooltip';
|
export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip';
|
||||||
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';
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
.MentionMenu {
|
.MentionTooltip {
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 useMentionMenu from './hooks/useMentionMenu';
|
import useMentionTooltip from './hooks/useMentionTooltip';
|
||||||
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 MentionMenu from './MentionMenu';
|
import MentionTooltip from './MentionTooltip';
|
||||||
|
|
||||||
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 {
|
||||||
isMentionMenuOpen, mentionFilter,
|
isMentionTooltipOpen, mentionFilter,
|
||||||
closeMentionMenu, insertMention,
|
closeMentionTooltip, insertMention,
|
||||||
mentionFilteredMembers,
|
mentionFilteredMembers,
|
||||||
} = useMentionMenu(
|
} = useMentionTooltip(
|
||||||
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">
|
||||||
<MentionMenu
|
<MentionTooltip
|
||||||
isOpen={isMentionMenuOpen}
|
isOpen={isMentionTooltipOpen}
|
||||||
onClose={closeMentionMenu}
|
onClose={closeMentionTooltip}
|
||||||
filter={mentionFilter}
|
filter={mentionFilter}
|
||||||
onInsertUserName={insertMention}
|
onInsertUserName={insertMention}
|
||||||
filteredChatMembers={mentionFilteredMembers}
|
filteredChatMembers={mentionFilteredMembers}
|
||||||
|
|||||||
@ -368,3 +368,37 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -55,8 +55,9 @@ 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 useMentionMenu from './hooks/useMentionMenu';
|
import useMentionTooltip from './hooks/useMentionTooltip';
|
||||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
@ -66,8 +67,9 @@ 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 MentionMenu from './MentionMenu.async';
|
import MentionTooltip from './MentionTooltip.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';
|
||||||
@ -112,6 +114,7 @@ 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;
|
||||||
@ -171,6 +174,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
contentToBeScheduled,
|
contentToBeScheduled,
|
||||||
shouldSuggestStickers,
|
shouldSuggestStickers,
|
||||||
|
recentEmojis,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
editMessage,
|
editMessage,
|
||||||
saveDraft,
|
saveDraft,
|
||||||
@ -254,10 +258,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const canShowCustomSendMenu = !shouldSchedule;
|
const canShowCustomSendMenu = !shouldSchedule;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isMentionMenuOpen, mentionFilter,
|
isMentionTooltipOpen, mentionFilter,
|
||||||
closeMentionMenu, insertMention,
|
closeMentionTooltip, insertMention,
|
||||||
mentionFilteredMembers,
|
mentionFilteredMembers,
|
||||||
} = useMentionMenu(
|
} = useMentionTooltip(
|
||||||
canSuggestMembers && !attachments.length,
|
canSuggestMembers && !attachments.length,
|
||||||
html,
|
html,
|
||||||
setHtml,
|
setHtml,
|
||||||
@ -281,11 +285,20 @@ 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 { isEmojiTooltipOpen, closeEmojiTooltip } = useEmojiTooltip(
|
const { isStickerTooltipOpen, closeStickerTooltip } = useStickerTooltip(
|
||||||
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()!;
|
||||||
@ -337,10 +350,11 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const resetComposer = useCallback(() => {
|
const resetComposer = useCallback(() => {
|
||||||
setHtml('');
|
setHtml('');
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
closeEmojiTooltip();
|
closeStickerTooltip();
|
||||||
closeCalendar();
|
closeCalendar();
|
||||||
setScheduledMessageArgs(undefined);
|
setScheduledMessageArgs(undefined);
|
||||||
closeMentionMenu();
|
closeMentionTooltip();
|
||||||
|
closeEmojiTooltip();
|
||||||
|
|
||||||
if (IS_MOBILE_SCREEN) {
|
if (IS_MOBILE_SCREEN) {
|
||||||
// @perf
|
// @perf
|
||||||
@ -348,7 +362,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
closeSymbolMenu();
|
closeSymbolMenu();
|
||||||
}
|
}
|
||||||
}, [closeEmojiTooltip, closeCalendar, closeMentionMenu, closeSymbolMenu]);
|
}, [closeStickerTooltip, closeCalendar, closeMentionTooltip, closeEmojiTooltip, closeSymbolMenu]);
|
||||||
|
|
||||||
// Handle chat change
|
// Handle chat change
|
||||||
const prevChatId = usePrevious(chatId);
|
const prevChatId = usePrevious(chatId);
|
||||||
@ -689,10 +703,10 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
message={renderedEditedMessage}
|
message={renderedEditedMessage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<MentionMenu
|
<MentionTooltip
|
||||||
isOpen={isMentionMenuOpen}
|
isOpen={isMentionTooltipOpen}
|
||||||
filter={mentionFilter}
|
filter={mentionFilter}
|
||||||
onClose={closeMentionMenu}
|
onClose={closeMentionTooltip}
|
||||||
onInsertUserName={insertMention}
|
onInsertUserName={insertMention}
|
||||||
filteredChatMembers={mentionFilteredMembers}
|
filteredChatMembers={mentionFilteredMembers}
|
||||||
usersById={usersById}
|
usersById={usersById}
|
||||||
@ -739,6 +753,7 @@ 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
|
||||||
@ -786,9 +801,15 @@ 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}
|
||||||
onStickerSelect={handleStickerSelect}
|
emojis={filteredEmojis}
|
||||||
|
onClose={closeEmojiTooltip}
|
||||||
|
onEmojiSelect={insertEmoji}
|
||||||
/>
|
/>
|
||||||
<AttachMenu
|
<AttachMenu
|
||||||
isOpen={isAttachMenuOpen}
|
isOpen={isAttachMenuOpen}
|
||||||
@ -909,6 +930,7 @@ 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, [
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,19 +6,20 @@ 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, onClick }) => {
|
const EmojiButton: FC<OwnProps> = ({ emoji, focus, 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"
|
className={`EmojiButton ${focus ? 'focus' : ''}`}
|
||||||
onClick={handleClick}
|
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" />}
|
{IS_EMOJI_SUPPORTED ? emoji.native : <img src={`/img-apple-64/${emoji.image}.png`} alt="" loading="lazy" />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,37 +1,11 @@
|
|||||||
.EmojiTooltip {
|
.EmojiTooltip {
|
||||||
position: absolute;
|
display: flex;
|
||||||
bottom: calc(100% + .5rem);
|
padding-left: .25rem;
|
||||||
left: 0;
|
overflow-x: auto;
|
||||||
width: 100%;
|
overflow-x: overlay;
|
||||||
background: var(--color-background);
|
overflow-y: hidden;
|
||||||
border-radius: var(--border-radius-messages);
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
max-height: 15rem;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
display: grid;
|
.EmojiButton {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(5rem, 1fr));
|
flex: 0 0 2.5rem
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,55 +1,110 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useEffect, useRef,
|
FC, memo, useCallback, useEffect, useRef, useState,
|
||||||
} 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 captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import cycleRestrict from '../../../util/cycleRestrict';
|
||||||
import { pick } from '../../../util/iteratees';
|
import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
|
||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
import findInViewport from '../../../util/findInViewport';
|
||||||
|
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 StickerButton from '../../common/StickerButton';
|
import EmojiButton from './EmojiButton';
|
||||||
|
|
||||||
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;
|
||||||
onStickerSelect: (sticker: ApiSticker) => void;
|
onEmojiSelect: (text: string) => void;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
emojis: Emoji[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
const EmojiTooltip: FC<OwnProps> = ({
|
||||||
stickers?: ApiSticker[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, 'clearStickersForEmoji'>;
|
|
||||||
|
|
||||||
const INTERSECTION_THROTTLE = 200;
|
|
||||||
|
|
||||||
const EmojiTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
|
|
||||||
isOpen,
|
isOpen,
|
||||||
onStickerSelect,
|
emojis,
|
||||||
stickers,
|
onClose,
|
||||||
clearStickersForEmoji,
|
onEmojiSelect,
|
||||||
}) => {
|
}) => {
|
||||||
// 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 {
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||||
observe: observeIntersection,
|
|
||||||
} = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE });
|
|
||||||
|
|
||||||
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 = () => {
|
const handleMouseEnter = () => {
|
||||||
document.body.classList.add('no-select');
|
document.body.classList.add('no-select');
|
||||||
@ -60,7 +115,7 @@ const EmojiTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'EmojiTooltip custom-scroll',
|
'EmojiTooltip composer-tooltip custom-scroll-x',
|
||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -71,15 +126,13 @@ const EmojiTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
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 && displayedStickers ? (
|
{shouldRender && emojis ? (
|
||||||
displayedStickers.map((sticker) => (
|
emojis.map((emoji, index) => (
|
||||||
<StickerButton
|
<EmojiButton
|
||||||
key={sticker.id}
|
key={emoji.id}
|
||||||
sticker={sticker}
|
emoji={emoji}
|
||||||
size={STICKER_SIZE_PICKER}
|
focus={selectedIndex === index}
|
||||||
observeIntersection={observeIntersection}
|
onClick={onEmojiSelect}
|
||||||
onClick={onStickerSelect}
|
|
||||||
clickArg={sticker}
|
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : shouldRender ? (
|
) : shouldRender ? (
|
||||||
@ -89,11 +142,4 @@ const EmojiTooltip: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(EmojiTooltip);
|
||||||
(global): StateProps => {
|
|
||||||
const { stickers } = global.stickers.forEmoji;
|
|
||||||
|
|
||||||
return { stickers };
|
|
||||||
},
|
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, ['clearStickersForEmoji']),
|
|
||||||
)(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 {
|
.MentionTooltip {
|
||||||
position: absolute;
|
|
||||||
bottom: calc(100% + .75rem);
|
|
||||||
left: 0;
|
|
||||||
width: calc(100% - 4rem);
|
width: calc(100% - 4rem);
|
||||||
max-width: 20rem;
|
max-width: 20rem;
|
||||||
background: var(--color-background);
|
flex-direction: column;
|
||||||
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;
|
||||||
|
|
||||||
@ -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 './MentionMenu.scss';
|
import './MentionTooltip.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 MentionMenu: FC<OwnProps> = ({
|
const MentionTooltip: FC<OwnProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
filter,
|
filter,
|
||||||
onClose,
|
onClose,
|
||||||
@ -136,7 +136,7 @@ const MentionMenu: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'MentionMenu custom-scroll',
|
'MentionTooltip composer-tooltip custom-scroll',
|
||||||
transitionClassNames,
|
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;
|
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;
|
||||||
@ -76,6 +77,7 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
placeholder,
|
placeholder,
|
||||||
shouldSetFocus,
|
shouldSetFocus,
|
||||||
shouldSupressFocus,
|
shouldSupressFocus,
|
||||||
|
shouldSupressTextFormatter,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onSupressedFocus,
|
onSupressedFocus,
|
||||||
onSend,
|
onSend,
|
||||||
@ -139,7 +141,8 @@ 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 (
|
||||||
!isSelectionInsideInput(selectionRange)
|
shouldSupressTextFormatter
|
||||||
|
|| !isSelectionInsideInput(selectionRange)
|
||||||
|| !selectedText
|
|| !selectedText
|
||||||
|| parseEmojiOnlyString(selectedText)
|
|| parseEmojiOnlyString(selectedText)
|
||||||
|| !selectionRange.START_TO_END
|
|| !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 {
|
||||||
import { getDispatch } from '../../../../lib/teact/teactn';
|
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(
|
export default function useEmojiTooltip(
|
||||||
isAllowed: boolean,
|
isAllowed: boolean,
|
||||||
html: string,
|
html: string,
|
||||||
stickers?: ApiSticker[],
|
recentEmojiIds: string[],
|
||||||
|
inputId = EDITABLE_INPUT_ID,
|
||||||
|
onUpdateHtml: (html: string) => void,
|
||||||
) {
|
) {
|
||||||
const { loadStickersForEmoji, clearStickersForEmoji } = getDispatch();
|
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
||||||
const isSingleEmoji = (
|
const [emojis, setEmojis] = useState<Emoji[]>([]);
|
||||||
(IS_EMOJI_SUPPORTED && parseEmojiOnlyString(html) === 1)
|
const [filteredEmojis, setFilteredEmojis] = useState<Emoji[]>([]);
|
||||||
|| (!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 (isAllowed && isSingleEmoji) {
|
if (!html || !emojis) {
|
||||||
loadStickersForEmoji({ emoji: html });
|
unmarkIsOpen();
|
||||||
} else if (hasStickers || !isSingleEmoji) {
|
return;
|
||||||
clearStickersForEmoji();
|
|
||||||
}
|
}
|
||||||
// We omit `hasStickers` here to prevent re-fetching after manually closing tooltip (via <Esc>).
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
const code = getEmojiCode(html);
|
||||||
}, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed]);
|
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 {
|
return {
|
||||||
isEmojiTooltipOpen: hasStickers,
|
isEmojiTooltipOpen: isOpen,
|
||||||
closeEmojiTooltip: clearStickersForEmoji,
|
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;
|
const RE_NOT_USERNAME_SEARCH = /[^@_\d\wа-яё]+/i;
|
||||||
|
|
||||||
export default function useMentionMenu(
|
export default function useMentionTooltip(
|
||||||
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 useMentionMenu(
|
|||||||
}, [html, inputId, onUpdateHtml, unmarkIsOpen]);
|
}, [html, inputId, onUpdateHtml, unmarkIsOpen]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isMentionMenuOpen: isOpen,
|
isMentionTooltipOpen: isOpen,
|
||||||
mentionFilter: currentFilter,
|
mentionFilter: currentFilter,
|
||||||
closeMentionMenu: unmarkIsOpen,
|
closeMentionTooltip: unmarkIsOpen,
|
||||||
insertMention,
|
insertMention,
|
||||||
mentionFilteredMembers: filteredMembers,
|
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 Handler = (e: KeyboardEvent) => void;
|
||||||
type CaptureOptions = Partial<Record<HandlerName, Handler>>;
|
type CaptureOptions = Partial<Record<HandlerName, Handler>>;
|
||||||
|
|
||||||
@ -10,6 +11,8 @@ 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',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -20,6 +23,8 @@ const handlers: Record<HandlerName, Handler[]> = {
|
|||||||
onEsc: [],
|
onEsc: [],
|
||||||
onUp: [],
|
onUp: [],
|
||||||
onDown: [],
|
onDown: [],
|
||||||
|
onLeft: [],
|
||||||
|
onRight: [],
|
||||||
onTab: [],
|
onTab: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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])) {
|
if (!EXCLUDE_EMOJIS.includes(emojiRaw[1][0])) {
|
||||||
category.emojis.push(emojiRaw[1]);
|
category.emojis.push(emojiRaw[1][0]);
|
||||||
emojiData.emojis[emojiRaw[1]] = {
|
emojiData.emojis[emojiRaw[1][0]] = {
|
||||||
id: emojiRaw[1],
|
id: emojiRaw[1][0],
|
||||||
colons: `:${emojiRaw[1]}:`,
|
names: emojiRaw[1] as string[],
|
||||||
native: unifiedToNative(emojiRaw[0]),
|
native: unifiedToNative(emojiRaw[0] as string),
|
||||||
image: emojiRaw[0].toLowerCase(),
|
image: (emojiRaw[0] as string).toLowerCase(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { IS_IOS } from './environment';
|
|||||||
|
|
||||||
const DURATION = 450;
|
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
|
// Native way seems to be smoother in Chrome
|
||||||
if (!IS_IOS) {
|
if (!IS_IOS) {
|
||||||
container.scrollTo({ left, behavior: 'smooth' });
|
container.scrollTo({ left, behavior: 'smooth' });
|
||||||
|
|||||||
@ -4,9 +4,10 @@ export default function findInViewport(
|
|||||||
margin = 0,
|
margin = 0,
|
||||||
isDense = false,
|
isDense = false,
|
||||||
shouldContainBottom = false,
|
shouldContainBottom = false,
|
||||||
|
isHorizontal = false,
|
||||||
) {
|
) {
|
||||||
const viewportY1 = container.scrollTop;
|
const viewportY1 = container[isHorizontal ? 'scrollLeft' : 'scrollTop'];
|
||||||
const viewportY2 = viewportY1 + container.offsetHeight;
|
const viewportY2 = viewportY1 + container[isHorizontal ? 'offsetWidth' : 'offsetHeight'];
|
||||||
const allElements = typeof selectorOrElements === 'string'
|
const allElements = typeof selectorOrElements === 'string'
|
||||||
? container.querySelectorAll<HTMLElement>(selectorOrElements)
|
? container.querySelectorAll<HTMLElement>(selectorOrElements)
|
||||||
: selectorOrElements;
|
: selectorOrElements;
|
||||||
@ -16,8 +17,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.offsetTop;
|
const y1 = element[isHorizontal ? 'offsetLeft' : 'offsetTop'];
|
||||||
const y2 = y1 + element.offsetHeight;
|
const y2 = y1 + element[isHorizontal ? 'offsetWidth' : '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;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
function isFullyVisible(container: HTMLElement, element: HTMLElement) {
|
function isFullyVisible(container: HTMLElement, element: HTMLElement, isHorizontal = false) {
|
||||||
const viewportY1 = container.scrollTop;
|
const viewportY1 = container[isHorizontal ? 'scrollLeft' : 'scrollTop'];
|
||||||
const viewportY2 = viewportY1 + container.offsetHeight;
|
const viewportY2 = viewportY1 + container[isHorizontal ? 'offsetWidth' : 'offsetHeight'];
|
||||||
const y1 = element.offsetTop;
|
const y1 = element[isHorizontal ? 'offsetLeft' : 'offsetTop'];
|
||||||
const y2 = y1 + element.offsetHeight;
|
const y2 = y1 + element[isHorizontal ? 'offsetWidth' : 'offsetHeight'];
|
||||||
return y1 > viewportY1 && y2 < viewportY2;
|
return y1 > viewportY1 && y2 < viewportY2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user