Attachment Modal: Add Symbol menu (#2525)
This commit is contained in:
parent
ecd1870fff
commit
30a36c7908
@ -175,7 +175,7 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
||||
{renderingStickerSet ? renderText(renderingStickerSet.title, ['emoji', 'links']) : lang('AccDescrStickerSet')}
|
||||
</div>
|
||||
<DropdownMenu
|
||||
className="stickers-more-menu"
|
||||
className="stickers-more-menu with-menu-transitions"
|
||||
trigger={MoreMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
|
||||
@ -226,7 +226,8 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
// switch back to the mobile version, you get a blank screen
|
||||
const { isDesktop } = useAppLayout();
|
||||
useEffect(() => {
|
||||
if (!isMiddleColumnOpen && !isLeftColumnOpen && !isDesktop) {
|
||||
const areColumnsConflicting = isLeftColumnOpen === isMiddleColumnOpen;
|
||||
if (areColumnsConflicting && !isDesktop) {
|
||||
toggleLeftColumn();
|
||||
}
|
||||
}, [isDesktop, isLeftColumnOpen, isMiddleColumnOpen, toggleLeftColumn]);
|
||||
|
||||
@ -355,7 +355,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
||||
</Button>
|
||||
<div className="modal-title">{bot?.firstName}</div>
|
||||
<DropdownMenu
|
||||
className="web-app-more-menu"
|
||||
className="web-app-more-menu with-menu-transitions"
|
||||
trigger={MoreMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
|
||||
@ -25,6 +25,64 @@
|
||||
max-height: calc(100vh - 3.25rem - 5rem);
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
|
||||
@supports not (padding-bottom: env(safe-area-inset-bottom)) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.symbol-menu-button {
|
||||
flex-shrink: 0;
|
||||
background: none !important;
|
||||
width: 3.5rem !important;
|
||||
height: 3.5rem !important;
|
||||
padding: 0 !important;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.symbol-menu-button, .mobile-symbol-menu-button {
|
||||
margin-right: -1.75rem;
|
||||
margin-left: -0.5rem !important;
|
||||
color: var(--color-composer-button);
|
||||
}
|
||||
|
||||
.mobile-symbol-menu-button {
|
||||
margin-left: 0 !important;
|
||||
margin-right: -1.25rem !important;
|
||||
width: 2.875rem;
|
||||
height: 2.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:global(body.keyboard-visible) & :global(.modal-content) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
&.mobile :global {
|
||||
.modal-dialog {
|
||||
margin: 0;
|
||||
max-width: 100% !important;
|
||||
align-self: end;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.mobile:global(:not(.open)) :global(.modal-dialog) {
|
||||
transform: translate3d(0, 8rem, 0);
|
||||
}
|
||||
|
||||
&.mobile.symbolMenuOpen :global(.modal-dialog) {
|
||||
transition: var(--layer-transition);
|
||||
|
||||
transform: translate3d(0, calc((var(--symbol-menu-footer-height) + var(--symbol-menu-height) - env(safe-area-inset-bottom)) * -1), 0);
|
||||
|
||||
@supports not (bottom: env(safe-area-inset-bottom)) {
|
||||
transform: translate3d(0, calc((var(--symbol-menu-footer-height) + var(--symbol-menu-height)) * -1), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,9 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiAttachment, ApiChatMember, ApiSticker } from '../../../api/types';
|
||||
import type {
|
||||
ApiAttachment, ApiChatMember, ApiSticker,
|
||||
} from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
|
||||
@ -46,6 +48,7 @@ import CustomEmojiTooltip from './CustomEmojiTooltip.async';
|
||||
import AttachmentModalItem from './AttachmentModalItem';
|
||||
import DropdownMenu from '../../ui/DropdownMenu';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
import SymbolMenuButton from './SymbolMenuButton';
|
||||
|
||||
import styles from './AttachmentModal.module.scss';
|
||||
|
||||
@ -66,6 +69,9 @@ export type OwnProps = {
|
||||
onClear: NoneToVoidFunction;
|
||||
onSendSilent: (sendCompressed: boolean, sendGrouped: boolean) => void;
|
||||
onSendScheduled: (sendCompressed: boolean, sendGrouped: boolean) => void;
|
||||
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
||||
onRemoveSymbol: VoidFunction;
|
||||
onEmojiSelect: (emoji: string) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -111,6 +117,9 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
onClear,
|
||||
onSendSilent,
|
||||
onSendScheduled,
|
||||
onCustomEmojiSelect,
|
||||
onRemoveSymbol,
|
||||
onEmojiSelect,
|
||||
}) => {
|
||||
const { addRecentCustomEmoji, addRecentEmoji, updateAttachmentSettings } = getActions();
|
||||
|
||||
@ -126,6 +135,8 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
const renderingAttachments = attachments.length ? attachments : prevAttachments;
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
||||
|
||||
const [shouldSendCompressed, setShouldSendCompressed] = useState(
|
||||
shouldSuggestCompression ?? attachmentSettings.shouldCompress,
|
||||
);
|
||||
@ -143,6 +154,12 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
const renderingIsOpen = Boolean(renderingAttachments?.length);
|
||||
const [isHovered, markHovered, unmarkHovered] = useFlag();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
closeSymbolMenu();
|
||||
}
|
||||
}, [closeSymbolMenu, isOpen]);
|
||||
|
||||
const [hasMedia, hasOnlyMedia] = useMemo(() => {
|
||||
const onlyMedia = Boolean(renderingAttachments?.every((a) => a.quick || a.audio));
|
||||
if (onlyMedia) return [true, true];
|
||||
@ -388,7 +405,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
</Button>
|
||||
<div className="modal-title">{title}</div>
|
||||
<DropdownMenu
|
||||
className="attachment-modal-more-menu"
|
||||
className="attachment-modal-more-menu with-menu-transitions"
|
||||
trigger={MoreMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
@ -453,6 +470,8 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
styles.root,
|
||||
isHovered && styles.hovered,
|
||||
!areAttachmentsNotScrolled && styles.headerBorder,
|
||||
isMobile && styles.mobile,
|
||||
isSymbolMenuOpen && styles.symbolMenuOpen,
|
||||
)}
|
||||
noBackdropClose
|
||||
>
|
||||
@ -517,6 +536,20 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
onClose={closeCustomEmojiTooltip}
|
||||
/>
|
||||
<div className={styles.caption}>
|
||||
<SymbolMenuButton
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
isMobile={isMobile}
|
||||
isReady={isReady}
|
||||
isSymbolMenuOpen={isSymbolMenuOpen}
|
||||
openSymbolMenu={openSymbolMenu}
|
||||
closeSymbolMenu={closeSymbolMenu}
|
||||
onCustomEmojiSelect={onCustomEmojiSelect}
|
||||
onRemoveSymbol={onRemoveSymbol}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
isAttachmentModal
|
||||
className="attachment-modal-symbol-menu with-menu-transitions"
|
||||
/>
|
||||
<MessageInput
|
||||
ref={inputRef}
|
||||
id="caption-input-text"
|
||||
@ -532,6 +565,8 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
onScroll={handleCaptionScroll}
|
||||
canAutoFocus={Boolean(isReady && isForCurrentMessageList && attachments.length)}
|
||||
captionLimit={leftChars}
|
||||
shouldSuppressFocus={isMobile && isSymbolMenuOpen}
|
||||
onSuppressedFocus={closeSymbolMenu}
|
||||
/>
|
||||
<div className={styles.sendWrapper}>
|
||||
<Button
|
||||
|
||||
@ -163,59 +163,60 @@
|
||||
animation: 0.25s ease-in-out forwards show-send-as-button;
|
||||
transform-origin: right;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-symbol-menu-button {
|
||||
width: 2.875rem;
|
||||
height: 2.875rem;
|
||||
position: relative;
|
||||
.mobile-symbol-menu-button {
|
||||
width: 2.875rem;
|
||||
height: 2.875rem;
|
||||
position: relative;
|
||||
|
||||
.icon-smile,
|
||||
.icon-keyboard,
|
||||
.icon-smile,
|
||||
.icon-keyboard,
|
||||
.Spinner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.Spinner {
|
||||
--spinner-size: 1.5rem;
|
||||
}
|
||||
|
||||
.icon-smile {
|
||||
animation: grow-icon 0.4s ease-out;
|
||||
}
|
||||
|
||||
.icon-keyboard,
|
||||
.Spinner {
|
||||
animation: hide-icon 0.4s forwards ease-out;
|
||||
}
|
||||
|
||||
&.not-ready > i {
|
||||
animation-duration: 0ms !important;
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
.Spinner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.Spinner {
|
||||
--spinner-size: 1.5rem;
|
||||
}
|
||||
|
||||
.icon-smile {
|
||||
animation: grow-icon 0.4s ease-out;
|
||||
}
|
||||
|
||||
.icon-keyboard,
|
||||
.icon-smile {
|
||||
animation: hide-icon 0.4s forwards ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-opened {
|
||||
.icon-keyboard {
|
||||
animation: grow-icon 0.4s ease-out;
|
||||
}
|
||||
|
||||
.icon-smile,
|
||||
.Spinner {
|
||||
animation: hide-icon 0.4s forwards ease-out;
|
||||
}
|
||||
|
||||
&.not-ready > i {
|
||||
animation-duration: 0ms !important;
|
||||
}
|
||||
|
||||
&.is-loading {
|
||||
.Spinner {
|
||||
animation: grow-icon 0.4s ease-out;
|
||||
}
|
||||
|
||||
.icon-keyboard,
|
||||
.icon-smile {
|
||||
animation: hide-icon 0.4s forwards ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
&.menu-opened {
|
||||
.icon-keyboard {
|
||||
animation: grow-icon 0.4s ease-out;
|
||||
}
|
||||
|
||||
.icon-smile,
|
||||
.Spinner {
|
||||
animation: hide-icon 0.4s forwards ease-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#message-compose {
|
||||
flex-grow: 1;
|
||||
max-width: calc(100% - 4rem);
|
||||
@ -396,6 +397,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.symbol-menu-trigger {
|
||||
left: -1rem;
|
||||
bottom: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.symbol-menu-button {
|
||||
width: 2rem !important;
|
||||
|
||||
@ -29,7 +29,7 @@ import {
|
||||
REPLIES_USER_ID,
|
||||
SEND_MESSAGE_ACTION_INTERVAL,
|
||||
EDITABLE_INPUT_CSS_SELECTOR,
|
||||
MAX_UPLOAD_FILEPART_SIZE,
|
||||
MAX_UPLOAD_FILEPART_SIZE, EDITABLE_INPUT_MODAL_ID,
|
||||
} from '../../../config';
|
||||
import { IS_VOICE_RECORDING_SUPPORTED, IS_IOS } from '../../../util/environment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
@ -93,7 +93,6 @@ import useInterval from '../../../hooks/useInterval';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
import useVoiceRecording from './hooks/useVoiceRecording';
|
||||
import useClipboardPaste from './hooks/useClipboardPaste';
|
||||
import useDraft from './hooks/useDraft';
|
||||
import useEditing from './hooks/useEditing';
|
||||
import useEmojiTooltip from './hooks/useEmojiTooltip';
|
||||
import useMentionTooltip from './hooks/useMentionTooltip';
|
||||
@ -105,6 +104,7 @@ import useAttachmentModal from './hooks/useAttachmentModal';
|
||||
import useGetSelectionRange from '../../../hooks/useGetSelectionRange';
|
||||
import useDerivedState from '../../../hooks/useDerivedState';
|
||||
import { useStateRef } from '../../../hooks/useStateRef';
|
||||
import useDraft from './hooks/useDraft';
|
||||
|
||||
import DeleteMessageModal from '../../common/DeleteMessageModal.async';
|
||||
import Button from '../../ui/Button';
|
||||
@ -112,7 +112,6 @@ import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
import AttachMenu from './AttachMenu';
|
||||
import Avatar from '../../common/Avatar';
|
||||
import SymbolMenu from './SymbolMenu.async';
|
||||
import InlineBotTooltip from './InlineBotTooltip.async';
|
||||
import MentionTooltip from './MentionTooltip.async';
|
||||
import CustomSendMenu from './CustomSendMenu.async';
|
||||
@ -130,6 +129,7 @@ import DropArea, { DropAreaState } from './DropArea.async';
|
||||
import WebPagePreview from './WebPagePreview';
|
||||
import SendAsMenu from './SendAsMenu.async';
|
||||
import BotMenuButton from './BotMenuButton';
|
||||
import SymbolMenuButton from './SymbolMenuButton';
|
||||
|
||||
import './Composer.scss';
|
||||
|
||||
@ -280,8 +280,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
sendMessage,
|
||||
clearDraft,
|
||||
showDialog,
|
||||
setStickerSearchQuery,
|
||||
setGifSearchQuery,
|
||||
forwardMessages,
|
||||
openPollModal,
|
||||
closePollModal,
|
||||
@ -374,7 +372,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
||||
const [isSendAsMenuOpen, openSendAsMenu, closeSendAsMenu] = useFlag();
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
||||
const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag();
|
||||
const [isHoverDisabled, disableHover, enableHover] = useFlag();
|
||||
|
||||
const {
|
||||
@ -525,31 +522,10 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
insertHtmlAndUpdateCursor(newHtml, inputId);
|
||||
}, [insertHtmlAndUpdateCursor]);
|
||||
|
||||
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
|
||||
const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html'])
|
||||
.join('')
|
||||
.replace(/\u200b+/g, '\u200b');
|
||||
insertHtmlAndUpdateCursor(newHtml, inputId);
|
||||
}, [insertHtmlAndUpdateCursor]);
|
||||
|
||||
const insertCustomEmojiAndUpdateCursor = useCallback((emoji: ApiSticker, inputId: string = EDITABLE_INPUT_ID) => {
|
||||
insertHtmlAndUpdateCursor(buildCustomEmojiHtml(emoji), inputId);
|
||||
}, [insertHtmlAndUpdateCursor]);
|
||||
|
||||
const removeSymbol = useCallback(() => {
|
||||
const selection = window.getSelection()!;
|
||||
|
||||
if (selection.rangeCount) {
|
||||
const selectionRange = selection.getRangeAt(0);
|
||||
if (isSelectionInsideInput(selectionRange, EDITABLE_INPUT_ID)) {
|
||||
document.execCommand('delete', false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setHtml(deleteLastCharacterOutsideSelection(getHtml()));
|
||||
}, [getHtml, setHtml]);
|
||||
|
||||
useDraft(draft, chatId, threadId, getHtml, setHtml, editingMessage, lastSyncTime);
|
||||
|
||||
const resetComposer = useCallback((shouldPreserveInput = false) => {
|
||||
@ -867,12 +843,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
openBotCommandMenu();
|
||||
}, [closeSymbolMenu, openBotCommandMenu]);
|
||||
|
||||
const handleActivateSymbolMenu = useCallback(() => {
|
||||
closeBotCommandMenu();
|
||||
closeSendAsMenu();
|
||||
openSymbolMenu();
|
||||
}, [closeBotCommandMenu, closeSendAsMenu, openSymbolMenu]);
|
||||
|
||||
const handleMessageSchedule = useCallback((
|
||||
args: ScheduledMessageArgs, scheduledAt: number,
|
||||
) => {
|
||||
@ -928,15 +898,40 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [handleFileSelect, requestedDraftFiles, resetOpenChatWithDraft]);
|
||||
|
||||
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker, inputId?: string) => {
|
||||
if (!emoji.isFree && !isCurrentUserPremium && !isChatWithSelf) {
|
||||
showCustomEmojiPremiumNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
insertCustomEmojiAndUpdateCursor(emoji);
|
||||
insertCustomEmojiAndUpdateCursor(emoji, inputId);
|
||||
}, [insertCustomEmojiAndUpdateCursor, isChatWithSelf, isCurrentUserPremium, showCustomEmojiPremiumNotification]);
|
||||
|
||||
const handleCustomEmojiSelectAttachmentModal = useCallback((emoji: ApiSticker) => {
|
||||
handleCustomEmojiSelect(emoji, EDITABLE_INPUT_MODAL_ID);
|
||||
}, [handleCustomEmojiSelect]);
|
||||
|
||||
const handleGifSelect = useCallback((gif: ApiVideo, isSilent?: boolean, isScheduleRequested?: boolean) => {
|
||||
if (shouldSchedule || isScheduleRequested) {
|
||||
forceShowSymbolMenu();
|
||||
requestCalendar((scheduledAt) => {
|
||||
cancelForceShowSymbolMenu();
|
||||
handleMessageSchedule({ gif, isSilent }, scheduledAt);
|
||||
requestAnimationFrame(() => {
|
||||
resetComposer(true);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendMessage({ gif, isSilent });
|
||||
requestAnimationFrame(() => {
|
||||
resetComposer(true);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
shouldSchedule, forceShowSymbolMenu, requestCalendar, cancelForceShowSymbolMenu, handleMessageSchedule,
|
||||
resetComposer, sendMessage,
|
||||
]);
|
||||
|
||||
const handleStickerSelect = useCallback((
|
||||
sticker: ApiSticker,
|
||||
isSilent?: boolean,
|
||||
@ -969,27 +964,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
resetComposer, sendMessage,
|
||||
]);
|
||||
|
||||
const handleGifSelect = useCallback((gif: ApiVideo, isSilent?: boolean, isScheduleRequested?: boolean) => {
|
||||
if (shouldSchedule || isScheduleRequested) {
|
||||
forceShowSymbolMenu();
|
||||
requestCalendar((scheduledAt) => {
|
||||
cancelForceShowSymbolMenu();
|
||||
handleMessageSchedule({ gif, isSilent }, scheduledAt);
|
||||
requestAnimationFrame(() => {
|
||||
resetComposer(true);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendMessage({ gif, isSilent });
|
||||
requestAnimationFrame(() => {
|
||||
resetComposer(true);
|
||||
});
|
||||
}
|
||||
}, [
|
||||
shouldSchedule, forceShowSymbolMenu, requestCalendar, cancelForceShowSymbolMenu, handleMessageSchedule,
|
||||
resetComposer, sendMessage,
|
||||
]);
|
||||
|
||||
const handleInlineBotSelect = useCallback((
|
||||
inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult, isSilent?: boolean, isScheduleRequested?: boolean,
|
||||
) => {
|
||||
@ -1059,31 +1033,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [handleMessageSchedule, handleSend, handleSendAttachments, requestCalendar, shouldSchedule]);
|
||||
|
||||
const handleSearchOpen = useCallback((type: 'stickers' | 'gifs') => {
|
||||
if (type === 'stickers') {
|
||||
setStickerSearchQuery({ query: '' });
|
||||
setGifSearchQuery({ query: undefined });
|
||||
} else {
|
||||
setGifSearchQuery({ query: '' });
|
||||
setStickerSearchQuery({ query: undefined });
|
||||
}
|
||||
}, [setStickerSearchQuery, setGifSearchQuery]);
|
||||
|
||||
const handleSymbolMenuOpen = useCallback(() => {
|
||||
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
||||
|
||||
if (!isMobile || messageInput !== document.activeElement) {
|
||||
openSymbolMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
messageInput?.blur();
|
||||
setTimeout(() => {
|
||||
closeBotCommandMenu();
|
||||
openSymbolMenu();
|
||||
}, MOBILE_KEYBOARD_HIDE_DELAY_MS);
|
||||
}, [openSymbolMenu, closeBotCommandMenu, isMobile]);
|
||||
|
||||
const handleSendAsMenuOpen = useCallback(() => {
|
||||
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
||||
|
||||
@ -1102,6 +1051,35 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}, MOBILE_KEYBOARD_HIDE_DELAY_MS);
|
||||
}, [closeBotCommandMenu, closeSymbolMenu, openSendAsMenu, isMobile]);
|
||||
|
||||
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
|
||||
const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html'])
|
||||
.join('')
|
||||
.replace(/\u200b+/g, '\u200b');
|
||||
insertHtmlAndUpdateCursor(newHtml, inputId);
|
||||
}, [insertHtmlAndUpdateCursor]);
|
||||
|
||||
const insertTextAndUpdateCursorAttachmentModal = useCallback((text: string) => {
|
||||
insertTextAndUpdateCursor(text, EDITABLE_INPUT_MODAL_ID);
|
||||
}, [insertTextAndUpdateCursor]);
|
||||
|
||||
const removeSymbol = useCallback((inputId = EDITABLE_INPUT_ID) => {
|
||||
const selection = window.getSelection()!;
|
||||
|
||||
if (selection.rangeCount) {
|
||||
const selectionRange = selection.getRangeAt(0);
|
||||
if (isSelectionInsideInput(selectionRange, inputId)) {
|
||||
document.execCommand('delete', false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setHtml(deleteLastCharacterOutsideSelection(getHtml()));
|
||||
}, [getHtml, setHtml]);
|
||||
|
||||
const removeSymbolAttachmentModal = useCallback(() => {
|
||||
removeSymbol(EDITABLE_INPUT_MODAL_ID);
|
||||
}, [removeSymbol]);
|
||||
|
||||
const handleAllScheduledClick = useCallback(() => {
|
||||
openChat({
|
||||
id: chatId, threadId, type: 'scheduled', noForumTopicPanel: true,
|
||||
@ -1193,14 +1171,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
isHoverDisabled && 'hover-disabled',
|
||||
);
|
||||
|
||||
const symbolMenuButtonClassName = buildClassName(
|
||||
'mobile-symbol-menu-button',
|
||||
!isReady && 'not-ready',
|
||||
isSymbolMenuLoaded
|
||||
? (isSymbolMenuOpen && 'menu-opened')
|
||||
: (isSymbolMenuOpen && 'is-loading'),
|
||||
);
|
||||
|
||||
const handleSendScheduled = useCallback(() => {
|
||||
requestCalendar((scheduledAt) => {
|
||||
handleMessageSchedule({}, scheduledAt);
|
||||
@ -1260,6 +1230,9 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
onFileAppend={handleAppendFiles}
|
||||
onClear={handleClearAttachments}
|
||||
onAttachmentsUpdate={handleSetAttachments}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelectAttachmentModal}
|
||||
onRemoveSymbol={removeSymbolAttachmentModal}
|
||||
onEmojiSelect={insertTextAndUpdateCursorAttachmentModal}
|
||||
/>
|
||||
<PollModal
|
||||
isOpen={pollModal.isOpen}
|
||||
@ -1359,29 +1332,25 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{isMobile ? (
|
||||
<Button
|
||||
className={symbolMenuButtonClassName}
|
||||
round
|
||||
color="translucent"
|
||||
onClick={isSymbolMenuOpen ? closeSymbolMenu : handleSymbolMenuOpen}
|
||||
ariaLabel="Choose emoji, sticker or GIF"
|
||||
>
|
||||
<i className="icon-smile" />
|
||||
<i className="icon-keyboard" />
|
||||
{isSymbolMenuOpen && !isSymbolMenuLoaded && <Spinner color="gray" />}
|
||||
</Button>
|
||||
) : (
|
||||
<ResponsiveHoverButton
|
||||
className={buildClassName('symbol-menu-button', isSymbolMenuOpen && 'activated')}
|
||||
round
|
||||
color="translucent"
|
||||
onActivate={handleActivateSymbolMenu}
|
||||
ariaLabel="Choose emoji, sticker or GIF"
|
||||
>
|
||||
<i className="icon-smile" />
|
||||
</ResponsiveHoverButton>
|
||||
)}
|
||||
<SymbolMenuButton
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
isMobile={isMobile}
|
||||
isReady={isReady}
|
||||
isSymbolMenuOpen={isSymbolMenuOpen}
|
||||
openSymbolMenu={openSymbolMenu}
|
||||
closeSymbolMenu={closeSymbolMenu}
|
||||
canSendStickers={canSendStickers}
|
||||
canSendGifs={canSendGifs}
|
||||
onGifSelect={handleGifSelect}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||
onRemoveSymbol={removeSymbol}
|
||||
onEmojiSelect={insertTextAndUpdateCursor}
|
||||
closeBotCommandMenu={closeBotCommandMenu}
|
||||
closeSendAsMenu={closeSendAsMenu}
|
||||
isSymbolMenuForced={isSymbolMenuForced}
|
||||
/>
|
||||
<MessageInput
|
||||
ref={inputRef}
|
||||
id="message-input-text"
|
||||
@ -1486,23 +1455,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
onCustomEmojiSelect={insertEmoji}
|
||||
onClose={closeEmojiTooltip}
|
||||
/>
|
||||
<SymbolMenu
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
isOpen={isSymbolMenuOpen || isSymbolMenuForced}
|
||||
canSendGifs={canSendGifs}
|
||||
canSendStickers={canSendStickers}
|
||||
onLoad={onSymbolMenuLoadingComplete}
|
||||
onClose={closeSymbolMenu}
|
||||
onEmojiSelect={insertTextAndUpdateCursor}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||
onGifSelect={handleGifSelect}
|
||||
onRemoveSymbol={removeSymbol}
|
||||
onSearchOpen={handleSearchOpen}
|
||||
addRecentEmoji={addRecentEmoji}
|
||||
addRecentCustomEmoji={addRecentCustomEmoji}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{activeVoiceRecording && (
|
||||
|
||||
@ -39,7 +39,7 @@ const CustomSendMenu: FC<OwnProps> = ({
|
||||
autoClose
|
||||
positionX="right"
|
||||
positionY={isOpenToBottom ? 'top' : 'bottom'}
|
||||
className="CustomSendMenu"
|
||||
className="CustomSendMenu with-menu-transitions"
|
||||
onClose={onClose}
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
|
||||
|
||||
@ -164,7 +164,8 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const selectCategory = useCallback((index: number) => {
|
||||
setActiveCategoryIndex(index);
|
||||
const categoryEl = document.getElementById(`emoji-category-${index}`)!;
|
||||
const categoryEl = containerRef.current!.closest<HTMLElement>('.SymbolMenu-main')!
|
||||
.querySelector(`#emoji-category-${index}`)! as HTMLElement;
|
||||
fastSmoothScroll(containerRef.current!, categoryEl, 'start', FOCUS_MARGIN, SMOOTH_SCROLL_DISTANCE);
|
||||
}, []);
|
||||
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
@import "../../../styles/mixins";
|
||||
|
||||
.SymbolMenu {
|
||||
&.attachment-modal-symbol-menu {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
&:not(.mobile-menu) {
|
||||
@media (max-height: 800px) {
|
||||
--symbol-menu-height: 40vh;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile-menu {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@ -19,7 +30,7 @@
|
||||
0
|
||||
);
|
||||
|
||||
&.open {
|
||||
&.open:not(.in-attachment-modal) {
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
||||
body.is-media-viewer-open & {
|
||||
@ -27,6 +38,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.open.in-attachment-modal {
|
||||
z-index: calc(var(--z-modal) + 1);
|
||||
transform: translate3d(
|
||||
0,
|
||||
calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height)),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Target: Old Firefox (Waterfox Classic)
|
||||
@supports not (padding-right: env(safe-area-inset-right)) {
|
||||
padding-right: 0;
|
||||
@ -107,6 +127,11 @@
|
||||
width: 3.5rem;
|
||||
height: 4.5rem;
|
||||
}
|
||||
|
||||
&.attachment-modal-symbol-menu > .backdrop {
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Remove this monster with context menu refactor
|
||||
|
||||
@ -35,24 +35,31 @@ export type OwnProps = {
|
||||
chatId: string;
|
||||
threadId?: number;
|
||||
isOpen: boolean;
|
||||
canSendStickers: boolean;
|
||||
canSendGifs: boolean;
|
||||
canSendStickers?: boolean;
|
||||
canSendGifs?: boolean;
|
||||
onLoad: () => void;
|
||||
onClose: () => void;
|
||||
onEmojiSelect: (emoji: string) => void;
|
||||
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
||||
onStickerSelect: (
|
||||
onStickerSelect?: (
|
||||
sticker: ApiSticker,
|
||||
isSilent?: boolean,
|
||||
shouldSchedule?: boolean,
|
||||
shouldPreserveInput?: boolean,
|
||||
shouldUpdateStickerSetsOrder?: boolean
|
||||
) => void;
|
||||
onGifSelect: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||
onGifSelect?: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||
onRemoveSymbol: () => void;
|
||||
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
||||
addRecentEmoji: GlobalActions['addRecentEmoji'];
|
||||
addRecentCustomEmoji: GlobalActions['addRecentCustomEmoji'];
|
||||
className?: string;
|
||||
isAttachmentModal?: boolean;
|
||||
positionX?: 'left' | 'right';
|
||||
positionY?: 'top' | 'bottom';
|
||||
transformOriginX?: number;
|
||||
transformOriginY?: number;
|
||||
style?: string;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -75,13 +82,20 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
onLoad,
|
||||
onClose,
|
||||
onEmojiSelect,
|
||||
isAttachmentModal,
|
||||
onCustomEmojiSelect,
|
||||
onStickerSelect,
|
||||
className,
|
||||
onGifSelect,
|
||||
onRemoveSymbol,
|
||||
onSearchOpen,
|
||||
addRecentEmoji,
|
||||
addRecentCustomEmoji,
|
||||
positionX,
|
||||
positionY,
|
||||
transformOriginX,
|
||||
transformOriginY,
|
||||
style,
|
||||
}) => {
|
||||
const { loadPremiumSetStickers, loadFeaturedEmojiStickers } = getActions();
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
@ -109,7 +123,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
}, [isCurrentUserPremium, lastSyncTime, loadFeaturedEmojiStickers, loadPremiumSetStickers]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isMobile) {
|
||||
if (!isMobile || isAttachmentModal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -128,7 +142,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [isMobile, isOpen]);
|
||||
}, [isAttachmentModal, isMobile, isOpen]);
|
||||
|
||||
const recentEmojisRef = useRef(recentEmojis);
|
||||
recentEmojisRef.current = recentEmojis;
|
||||
@ -180,7 +194,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
const handleStickerSelect = useCallback((
|
||||
sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean, shouldUpdateStickerSetsOrder?: boolean,
|
||||
) => {
|
||||
onStickerSelect(sticker, isSilent, shouldSchedule, true, shouldUpdateStickerSetsOrder);
|
||||
onStickerSelect?.(sticker, isSilent, shouldSchedule, true, shouldUpdateStickerSetsOrder);
|
||||
}, [onStickerSelect]);
|
||||
|
||||
const lang = useLang();
|
||||
@ -204,6 +218,8 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
);
|
||||
case SymbolMenuTabs.Stickers:
|
||||
if (!canSendStickers) return undefined;
|
||||
|
||||
return (
|
||||
<StickerPicker
|
||||
className="picker-tab"
|
||||
@ -215,6 +231,8 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
);
|
||||
case SymbolMenuTabs.GIFs:
|
||||
if (!canSendGifs || !onGifSelect) return undefined;
|
||||
|
||||
return (
|
||||
<GifPicker
|
||||
className="picker-tab"
|
||||
@ -259,6 +277,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
onSwitchTab={setActiveTab}
|
||||
onRemoveSymbol={onRemoveSymbol}
|
||||
onSearchOpen={handleSearch}
|
||||
isAttachmentModal={isAttachmentModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@ -268,15 +287,24 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const className = buildClassName(
|
||||
const mobileClassName = buildClassName(
|
||||
'SymbolMenu mobile-menu',
|
||||
transitionClassNames,
|
||||
isLeftColumnShown && 'left-column-open',
|
||||
isAttachmentModal && 'in-attachment-modal',
|
||||
);
|
||||
|
||||
if (isAttachmentModal) {
|
||||
return (
|
||||
<div className={mobileClassName}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div className={className}>
|
||||
<div className={mobileClassName}>
|
||||
{content}
|
||||
</div>
|
||||
</Portal>
|
||||
@ -286,15 +314,19 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<Menu
|
||||
isOpen={isOpen}
|
||||
positionX="left"
|
||||
positionY="bottom"
|
||||
positionX={isAttachmentModal ? positionX : 'left'}
|
||||
positionY={isAttachmentModal ? positionY : 'bottom'}
|
||||
onClose={onClose}
|
||||
className="SymbolMenu"
|
||||
withPortal={isAttachmentModal}
|
||||
className={buildClassName('SymbolMenu', className)}
|
||||
onCloseAnimationEnd={onClose}
|
||||
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
|
||||
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
|
||||
noCloseOnBackdrop={!IS_TOUCH_ENV}
|
||||
noCompact
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
style={style}
|
||||
>
|
||||
{content}
|
||||
</Menu>
|
||||
|
||||
210
src/components/middle/composer/SymbolMenuButton.tsx
Normal file
210
src/components/middle/composer/SymbolMenuButton.tsx
Normal file
@ -0,0 +1,210 @@
|
||||
import React, {
|
||||
memo, useCallback, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { IAnchorPosition } from '../../../types';
|
||||
import type { ApiVideo, ApiSticker } from '../../../api/types';
|
||||
|
||||
import { EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_MODAL_CSS_SELECTOR } from '../../../config';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
import ResponsiveHoverButton from '../../ui/ResponsiveHoverButton';
|
||||
import SymbolMenu from './SymbolMenu.async';
|
||||
|
||||
const MOBILE_KEYBOARD_HIDE_DELAY_MS = 100;
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
threadId?: number;
|
||||
isMobile?: boolean;
|
||||
isReady?: boolean;
|
||||
isSymbolMenuOpen?: boolean;
|
||||
canSendGifs?: boolean;
|
||||
canSendStickers?: boolean;
|
||||
openSymbolMenu: VoidFunction;
|
||||
closeSymbolMenu: VoidFunction;
|
||||
onCustomEmojiSelect: (emoji: ApiSticker) => void;
|
||||
onStickerSelect?: (
|
||||
sticker: ApiSticker,
|
||||
isSilent?: boolean,
|
||||
shouldSchedule?: boolean,
|
||||
shouldPreserveInput?: boolean,
|
||||
shouldUpdateStickerSetsOrder?: boolean
|
||||
) => void;
|
||||
onGifSelect?: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||
onRemoveSymbol: VoidFunction;
|
||||
onEmojiSelect: (emoji: string) => void;
|
||||
closeBotCommandMenu?: VoidFunction;
|
||||
closeSendAsMenu?: VoidFunction;
|
||||
isSymbolMenuForced?: boolean;
|
||||
isAttachmentModal?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const SymbolMenuButton: FC<OwnProps> = ({
|
||||
chatId,
|
||||
threadId,
|
||||
isMobile,
|
||||
canSendGifs,
|
||||
canSendStickers,
|
||||
isReady,
|
||||
isSymbolMenuOpen,
|
||||
openSymbolMenu,
|
||||
closeSymbolMenu,
|
||||
onCustomEmojiSelect,
|
||||
onStickerSelect,
|
||||
onGifSelect,
|
||||
isAttachmentModal,
|
||||
onRemoveSymbol,
|
||||
onEmojiSelect,
|
||||
closeBotCommandMenu,
|
||||
closeSendAsMenu,
|
||||
isSymbolMenuForced,
|
||||
className,
|
||||
}) => {
|
||||
const {
|
||||
setStickerSearchQuery,
|
||||
setGifSearchQuery,
|
||||
addRecentEmoji,
|
||||
addRecentCustomEmoji,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag();
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<IAnchorPosition | undefined>(undefined);
|
||||
|
||||
const symbolMenuButtonClassName = buildClassName(
|
||||
'mobile-symbol-menu-button',
|
||||
!isReady && 'not-ready',
|
||||
isSymbolMenuLoaded
|
||||
? (isSymbolMenuOpen && 'menu-opened')
|
||||
: (isSymbolMenuOpen && 'is-loading'),
|
||||
);
|
||||
|
||||
const handleActivateSymbolMenu = useCallback(() => {
|
||||
closeBotCommandMenu?.();
|
||||
closeSendAsMenu?.();
|
||||
openSymbolMenu();
|
||||
const triggerEl = triggerRef.current;
|
||||
if (!triggerEl) return;
|
||||
const { x, y } = triggerEl.getBoundingClientRect();
|
||||
setContextMenuPosition({ x, y });
|
||||
}, [closeBotCommandMenu, closeSendAsMenu, openSymbolMenu]);
|
||||
|
||||
const handleSearchOpen = useCallback((type: 'stickers' | 'gifs') => {
|
||||
if (type === 'stickers') {
|
||||
setStickerSearchQuery({ query: '' });
|
||||
setGifSearchQuery({ query: undefined });
|
||||
} else {
|
||||
setGifSearchQuery({ query: '' });
|
||||
setStickerSearchQuery({ query: undefined });
|
||||
}
|
||||
}, [setStickerSearchQuery, setGifSearchQuery]);
|
||||
|
||||
const handleSymbolMenuOpen = useCallback(() => {
|
||||
const messageInput = document.querySelector<HTMLDivElement>(
|
||||
isAttachmentModal ? EDITABLE_INPUT_MODAL_CSS_SELECTOR : EDITABLE_INPUT_CSS_SELECTOR,
|
||||
);
|
||||
|
||||
if (!isMobile || messageInput !== document.activeElement) {
|
||||
openSymbolMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
messageInput?.blur();
|
||||
setTimeout(() => {
|
||||
closeBotCommandMenu?.();
|
||||
openSymbolMenu();
|
||||
}, MOBILE_KEYBOARD_HIDE_DELAY_MS);
|
||||
}, [isAttachmentModal, isMobile, openSymbolMenu, closeBotCommandMenu]);
|
||||
|
||||
const getTriggerElement = useCallback(() => triggerRef.current, []);
|
||||
|
||||
const getRootElement = useCallback(
|
||||
() => triggerRef.current?.closest('.custom-scroll, .no-scrollbar'),
|
||||
[],
|
||||
);
|
||||
|
||||
const getMenuElement = useCallback(
|
||||
() => document.querySelector('#portals .SymbolMenu .bubble'),
|
||||
[],
|
||||
);
|
||||
|
||||
const getLayout = useCallback(() => ({
|
||||
withPortal: true,
|
||||
}), []);
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useContextMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<Button
|
||||
className={symbolMenuButtonClassName}
|
||||
round
|
||||
color="translucent"
|
||||
onClick={isSymbolMenuOpen ? closeSymbolMenu : handleSymbolMenuOpen}
|
||||
ariaLabel="Choose emoji, sticker or GIF"
|
||||
>
|
||||
<i className="icon-smile" />
|
||||
<i className="icon-keyboard" />
|
||||
{isSymbolMenuOpen && !isSymbolMenuLoaded && <Spinner color="gray" />}
|
||||
</Button>
|
||||
) : (
|
||||
<ResponsiveHoverButton
|
||||
className={buildClassName('symbol-menu-button', isSymbolMenuOpen && 'activated')}
|
||||
round
|
||||
color="translucent"
|
||||
onActivate={handleActivateSymbolMenu}
|
||||
ariaLabel="Choose emoji, sticker or GIF"
|
||||
>
|
||||
<div ref={triggerRef} className="symbol-menu-trigger" />
|
||||
<i className="icon-smile" />
|
||||
</ResponsiveHoverButton>
|
||||
)}
|
||||
|
||||
<SymbolMenu
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
isOpen={isSymbolMenuOpen || Boolean(isSymbolMenuForced)}
|
||||
canSendGifs={canSendGifs}
|
||||
canSendStickers={canSendStickers}
|
||||
onLoad={onSymbolMenuLoadingComplete}
|
||||
onClose={closeSymbolMenu}
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
onStickerSelect={onStickerSelect}
|
||||
onCustomEmojiSelect={onCustomEmojiSelect}
|
||||
onGifSelect={onGifSelect}
|
||||
onRemoveSymbol={onRemoveSymbol}
|
||||
onSearchOpen={handleSearchOpen}
|
||||
addRecentEmoji={addRecentEmoji}
|
||||
addRecentCustomEmoji={addRecentCustomEmoji}
|
||||
isAttachmentModal={isAttachmentModal}
|
||||
className={className}
|
||||
positionX={isAttachmentModal ? positionX : undefined}
|
||||
positionY={isAttachmentModal ? positionY : undefined}
|
||||
transformOriginX={isAttachmentModal ? transformOriginX : undefined}
|
||||
transformOriginY={isAttachmentModal ? transformOriginY : undefined}
|
||||
style={isAttachmentModal ? menuStyle : undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SymbolMenuButton);
|
||||
@ -10,6 +10,7 @@ type OwnProps = {
|
||||
onSwitchTab: (tab: SymbolMenuTabs) => void;
|
||||
onRemoveSymbol: () => void;
|
||||
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
||||
isAttachmentModal?: boolean;
|
||||
};
|
||||
|
||||
export enum SymbolMenuTabs {
|
||||
@ -34,7 +35,7 @@ const SYMBOL_MENU_TAB_ICONS = {
|
||||
};
|
||||
|
||||
const SymbolMenuFooter: FC<OwnProps> = ({
|
||||
activeTab, onSwitchTab, onRemoveSymbol, onSearchOpen,
|
||||
activeTab, onSwitchTab, onRemoveSymbol, onSearchOpen, isAttachmentModal,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
@ -79,8 +80,8 @@ const SymbolMenuFooter: FC<OwnProps> = ({
|
||||
|
||||
{renderTabButton(SymbolMenuTabs.Emoji)}
|
||||
{renderTabButton(SymbolMenuTabs.CustomEmoji)}
|
||||
{renderTabButton(SymbolMenuTabs.Stickers)}
|
||||
{renderTabButton(SymbolMenuTabs.GIFs)}
|
||||
{!isAttachmentModal && renderTabButton(SymbolMenuTabs.Stickers)}
|
||||
{!isAttachmentModal && renderTabButton(SymbolMenuTabs.GIFs)}
|
||||
|
||||
{(activeTab === SymbolMenuTabs.Emoji || activeTab === SymbolMenuTabs.CustomEmoji) && (
|
||||
<Button
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
body.has-open-dialog &:not(.CustomSendMenu):not(.web-app-more-menu):not(.attachment-modal-more-menu):not(.stickers-more-menu) .bubble {
|
||||
body.has-open-dialog &:not(.with-menu-transitions) .bubble {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useRef, useCallback, memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps as ButtonProps } from './Button';
|
||||
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
|
||||
import type { OwnProps as ButtonProps } from './Button';
|
||||
import Button from './Button';
|
||||
|
||||
type OwnProps = {
|
||||
onActivate: NoneToVoidFunction;
|
||||
onActivate: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
} & Omit<ButtonProps, (
|
||||
'onClick' | 'onMouseDown' |
|
||||
'onMouseEnter' | 'onMouseLeave' |
|
||||
@ -21,13 +22,13 @@ let isFirstTimeActivation = true;
|
||||
const ResponsiveHoverButton: FC<OwnProps> = ({ onActivate, ...buttonProps }) => {
|
||||
const isMouseInside = useRef(false);
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
const handleMouseEnter = useCallback((e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
isMouseInside.current = true;
|
||||
|
||||
// This is used to counter additional delay caused by asynchronous module loading
|
||||
if (isFirstTimeActivation) {
|
||||
isFirstTimeActivation = false;
|
||||
onActivate();
|
||||
onActivate(e);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -37,7 +38,7 @@ const ResponsiveHoverButton: FC<OwnProps> = ({ onActivate, ...buttonProps }) =>
|
||||
}
|
||||
openTimeout = window.setTimeout(() => {
|
||||
if (isMouseInside.current) {
|
||||
onActivate();
|
||||
onActivate(e);
|
||||
}
|
||||
}, BUTTON_ACTIVATE_DELAY);
|
||||
}, [onActivate]);
|
||||
@ -46,9 +47,9 @@ const ResponsiveHoverButton: FC<OwnProps> = ({ onActivate, ...buttonProps }) =>
|
||||
isMouseInside.current = false;
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
isMouseInside.current = true;
|
||||
onActivate();
|
||||
onActivate(e);
|
||||
}, [onActivate]);
|
||||
|
||||
return (
|
||||
|
||||
@ -111,6 +111,7 @@ export const EDITABLE_INPUT_ID = 'editable-message-text';
|
||||
export const EDITABLE_INPUT_MODAL_ID = 'editable-message-text-modal';
|
||||
// eslint-disable-next-line max-len
|
||||
export const EDITABLE_INPUT_CSS_SELECTOR = `.messages-layout .Transition__slide--active #${EDITABLE_INPUT_ID}, .messages-layout .Transition > .to #${EDITABLE_INPUT_ID}`;
|
||||
export const EDITABLE_INPUT_MODAL_CSS_SELECTOR = `#${EDITABLE_INPUT_MODAL_ID}`;
|
||||
|
||||
export const CUSTOM_APPENDIX_ATTRIBUTE = 'data-has-custom-appendix';
|
||||
export const MESSAGE_CONTENT_CLASS_NAME = 'message-content';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user