From 30a36c790882cfd60a01459803b9349088ca4d7c Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Mon, 13 Feb 2023 03:32:27 +0100 Subject: [PATCH] Attachment Modal: Add Symbol menu (#2525) --- src/components/common/StickerSetModal.tsx | 2 +- src/components/main/Main.tsx | 3 +- src/components/main/WebAppModal.tsx | 2 +- .../composer/AttachmentModal.module.scss | 58 +++++ .../middle/composer/AttachmentModal.tsx | 39 +++- src/components/middle/composer/Composer.scss | 87 ++++---- src/components/middle/composer/Composer.tsx | 210 +++++++----------- .../middle/composer/CustomSendMenu.tsx | 2 +- .../middle/composer/EmojiPicker.tsx | 3 +- .../middle/composer/SymbolMenu.scss | 27 ++- src/components/middle/composer/SymbolMenu.tsx | 56 ++++- .../middle/composer/SymbolMenuButton.tsx | 210 ++++++++++++++++++ .../middle/composer/SymbolMenuFooter.tsx | 7 +- src/components/ui/Menu.scss | 2 +- src/components/ui/ResponsiveHoverButton.tsx | 15 +- src/config.ts | 1 + 16 files changed, 524 insertions(+), 200 deletions(-) create mode 100644 src/components/middle/composer/SymbolMenuButton.tsx diff --git a/src/components/common/StickerSetModal.tsx b/src/components/common/StickerSetModal.tsx index 06b3f8776..e5820eaf8 100644 --- a/src/components/common/StickerSetModal.tsx +++ b/src/components/common/StickerSetModal.tsx @@ -175,7 +175,7 @@ const StickerSetModal: FC = ({ {renderingStickerSet ? renderText(renderingStickerSet.title, ['emoji', 'links']) : lang('AccDescrStickerSet')} diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 610efea43..4835bc49f 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -226,7 +226,8 @@ const Main: FC = ({ // 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]); diff --git a/src/components/main/WebAppModal.tsx b/src/components/main/WebAppModal.tsx index fdb99ecd0..77f866ab5 100644 --- a/src/components/main/WebAppModal.tsx +++ b/src/components/main/WebAppModal.tsx @@ -355,7 +355,7 @@ const WebAppModal: FC = ({
{bot?.firstName}
diff --git a/src/components/middle/composer/AttachmentModal.module.scss b/src/components/middle/composer/AttachmentModal.module.scss index 7eaf6c644..9f39c7317 100644 --- a/src/components/middle/composer/AttachmentModal.module.scss +++ b/src/components/middle/composer/AttachmentModal.module.scss @@ -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); } } diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index 1100f650c..9f3135946 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -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 = ({ onClear, onSendSilent, onSendScheduled, + onCustomEmojiSelect, + onRemoveSymbol, + onEmojiSelect, }) => { const { addRecentCustomEmoji, addRecentEmoji, updateAttachmentSettings } = getActions(); @@ -126,6 +135,8 @@ const AttachmentModal: FC = ({ 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 = ({ 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 = ({
{title}
@@ -453,6 +470,8 @@ const AttachmentModal: FC = ({ styles.root, isHovered && styles.hovered, !areAttachmentsNotScrolled && styles.headerBorder, + isMobile && styles.mobile, + isSymbolMenuOpen && styles.symbolMenuOpen, )} noBackdropClose > @@ -517,6 +536,20 @@ const AttachmentModal: FC = ({ onClose={closeCustomEmojiTooltip} />
+ = ({ onScroll={handleCaptionScroll} canAutoFocus={Boolean(isReady && isForCurrentMessageList && attachments.length)} captionLimit={leftChars} + shouldSuppressFocus={isMobile && isSymbolMenuOpen} + onSuppressedFocus={closeSymbolMenu} />
)} - {isMobile ? ( - - ) : ( - - - - )} + = ({ onCustomEmojiSelect={insertEmoji} onClose={closeEmojiTooltip} /> -
{activeVoiceRecording && ( diff --git a/src/components/middle/composer/CustomSendMenu.tsx b/src/components/middle/composer/CustomSendMenu.tsx index 2785b4440..9d48b04eb 100644 --- a/src/components/middle/composer/CustomSendMenu.tsx +++ b/src/components/middle/composer/CustomSendMenu.tsx @@ -39,7 +39,7 @@ const CustomSendMenu: FC = ({ autoClose positionX="right" positionY={isOpenToBottom ? 'top' : 'bottom'} - className="CustomSendMenu" + className="CustomSendMenu with-menu-transitions" onClose={onClose} onCloseAnimationEnd={onCloseAnimationEnd} onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined} diff --git a/src/components/middle/composer/EmojiPicker.tsx b/src/components/middle/composer/EmojiPicker.tsx index a6a20321f..d9395f873 100644 --- a/src/components/middle/composer/EmojiPicker.tsx +++ b/src/components/middle/composer/EmojiPicker.tsx @@ -164,7 +164,8 @@ const EmojiPicker: FC = ({ const selectCategory = useCallback((index: number) => { setActiveCategoryIndex(index); - const categoryEl = document.getElementById(`emoji-category-${index}`)!; + const categoryEl = containerRef.current!.closest('.SymbolMenu-main')! + .querySelector(`#emoji-category-${index}`)! as HTMLElement; fastSmoothScroll(containerRef.current!, categoryEl, 'start', FOCUS_MARGIN, SMOOTH_SCROLL_DISTANCE); }, []); diff --git a/src/components/middle/composer/SymbolMenu.scss b/src/components/middle/composer/SymbolMenu.scss index d8d22efe5..1c5a68f62 100644 --- a/src/components/middle/composer/SymbolMenu.scss +++ b/src/components/middle/composer/SymbolMenu.scss @@ -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 diff --git a/src/components/middle/composer/SymbolMenu.tsx b/src/components/middle/composer/SymbolMenu.tsx index adb447217..35f4a57dc 100644 --- a/src/components/middle/composer/SymbolMenu.tsx +++ b/src/components/middle/composer/SymbolMenu.tsx @@ -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 = ({ 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(0); @@ -109,7 +123,7 @@ const SymbolMenu: FC = ({ }, [isCurrentUserPremium, lastSyncTime, loadFeaturedEmojiStickers, loadPremiumSetStickers]); useLayoutEffect(() => { - if (!isMobile) { + if (!isMobile || isAttachmentModal) { return undefined; } @@ -128,7 +142,7 @@ const SymbolMenu: FC = ({ }); } }; - }, [isMobile, isOpen]); + }, [isAttachmentModal, isMobile, isOpen]); const recentEmojisRef = useRef(recentEmojis); recentEmojisRef.current = recentEmojis; @@ -180,7 +194,7 @@ const SymbolMenu: FC = ({ 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 = ({ /> ); case SymbolMenuTabs.Stickers: + if (!canSendStickers) return undefined; + return ( = ({ /> ); case SymbolMenuTabs.GIFs: + if (!canSendGifs || !onGifSelect) return undefined; + return ( = ({ onSwitchTab={setActiveTab} onRemoveSymbol={onRemoveSymbol} onSearchOpen={handleSearch} + isAttachmentModal={isAttachmentModal} /> ); @@ -268,15 +287,24 @@ const SymbolMenu: FC = ({ return undefined; } - const className = buildClassName( + const mobileClassName = buildClassName( 'SymbolMenu mobile-menu', transitionClassNames, isLeftColumnShown && 'left-column-open', + isAttachmentModal && 'in-attachment-modal', ); + if (isAttachmentModal) { + return ( +
+ {content} +
+ ); + } + return ( -
+
{content}
@@ -286,15 +314,19 @@ const SymbolMenu: FC = ({ return ( {content} diff --git a/src/components/middle/composer/SymbolMenuButton.tsx b/src/components/middle/composer/SymbolMenuButton.tsx new file mode 100644 index 000000000..4c564878a --- /dev/null +++ b/src/components/middle/composer/SymbolMenuButton.tsx @@ -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 = ({ + 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(null); + + const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag(); + const [contextMenuPosition, setContextMenuPosition] = useState(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( + 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 ? ( + + ) : ( + +
+ + + )} + + + + ); +}; + +export default memo(SymbolMenuButton); diff --git a/src/components/middle/composer/SymbolMenuFooter.tsx b/src/components/middle/composer/SymbolMenuFooter.tsx index abb683f61..393084b93 100644 --- a/src/components/middle/composer/SymbolMenuFooter.tsx +++ b/src/components/middle/composer/SymbolMenuFooter.tsx @@ -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 = ({ - activeTab, onSwitchTab, onRemoveSymbol, onSearchOpen, + activeTab, onSwitchTab, onRemoveSymbol, onSearchOpen, isAttachmentModal, }) => { const lang = useLang(); @@ -79,8 +80,8 @@ const SymbolMenuFooter: FC = ({ {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) && (