/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {observer} from 'mobx-react-lite'; import React from 'react'; import {EmojiPickerCategoryList} from '~/components/channel/emoji-picker/EmojiPickerCategoryList'; import {EMOJI_SPRITE_SIZE} from '~/components/channel/emoji-picker/EmojiPickerConstants'; import {EmojiPickerSearchBar} from '~/components/channel/emoji-picker/EmojiPickerSearchBar'; import {useEmojiCategories} from '~/components/channel/emoji-picker/hooks/useEmojiCategories'; import {useVirtualRows} from '~/components/channel/emoji-picker/hooks/useVirtualRows'; import {VirtualizedRow} from '~/components/channel/emoji-picker/VirtualRow'; import mobileStyles from '~/components/channel/MobileEmojiPicker.module.css'; import { ExpressionPickerHeaderPortal, useExpressionPickerHeaderPortal, } from '~/components/popouts/ExpressionPickerPopout'; import {Scroller, type ScrollerHandle} from '~/components/uikit/Scroller'; import {useForceUpdate} from '~/hooks/useForceUpdate'; import {ComponentDispatch} from '~/lib/ComponentDispatch'; import UnicodeEmojis, {EMOJI_SPRITES} from '~/lib/UnicodeEmojis'; import ChannelStore from '~/stores/ChannelStore'; import EmojiStore, {type Emoji, normalizeEmojiSearchQuery} from '~/stores/EmojiStore'; export const MobileEmojiPicker = observer( ({ channelId, handleSelect, externalSearchTerm, externalSetSearchTerm, hideSearchBar = false, }: { channelId?: string; handleSelect: (emoji: Emoji, shiftKey?: boolean) => void; externalSearchTerm?: string; externalSetSearchTerm?: (term: string) => void; hideSearchBar?: boolean; }) => { const headerPortalContext = useExpressionPickerHeaderPortal(); const hasPortal = Boolean(headerPortalContext?.headerPortalElement); const [internalSearchTerm, setInternalSearchTerm] = React.useState(''); const [hoveredEmoji, setHoveredEmoji] = React.useState(null); const [renderedEmojis, setRenderedEmojis] = React.useState>([]); const [allEmojis, setAllEmojis] = React.useState>([]); const scrollerRef = React.useRef(null); const emojiRefs = React.useRef>(new Map()); const channel = channelId ? (ChannelStore.getChannel(channelId) ?? null) : null; const categoryRefs = React.useRef>(new Map()); const forceUpdate = useForceUpdate(); const skinTone = EmojiStore.skinTone; const searchTerm = externalSearchTerm ?? internalSearchTerm; const setSearchTerm = externalSetSearchTerm ?? setInternalSearchTerm; const normalizedSearchTerm = React.useMemo(() => normalizeEmojiSearchQuery(searchTerm), [searchTerm]); const spriteSheetSizes = React.useMemo(() => { const nonDiversitySize = [ `${EMOJI_SPRITE_SIZE * EMOJI_SPRITES.NonDiversityPerRow}px`, `${EMOJI_SPRITE_SIZE * Math.ceil(UnicodeEmojis.numNonDiversitySprites / EMOJI_SPRITES.NonDiversityPerRow)}px`, ].join(' '); const diversitySize = [ `${EMOJI_SPRITE_SIZE * EMOJI_SPRITES.DiversityPerRow}px`, `${EMOJI_SPRITE_SIZE * Math.ceil(UnicodeEmojis.numDiversitySprites / EMOJI_SPRITES.DiversityPerRow)}px`, ].join(' '); return {nonDiversitySize, diversitySize}; }, []); React.useEffect(() => { const emojis = EmojiStore.search(channel, normalizedSearchTerm).slice(); setRenderedEmojis(emojis); }, [channel, normalizedSearchTerm]); React.useEffect(() => { const emojis = EmojiStore.search(channel, '').slice(); setAllEmojis(emojis); }, [channel]); React.useEffect(() => { return ComponentDispatch.subscribe('EMOJI_PICKER_RERENDER', forceUpdate); }); const {customEmojisByGuildId, unicodeEmojisByCategory, favoriteEmojis, frequentlyUsedEmojis} = useEmojiCategories( allEmojis, renderedEmojis, ); const showFrequentlyUsedButton = frequentlyUsedEmojis.length > 0 && !normalizedSearchTerm; const virtualRows = useVirtualRows( normalizedSearchTerm, renderedEmojis, favoriteEmojis, frequentlyUsedEmojis, customEmojisByGuildId, unicodeEmojisByCategory, 8, ); const handleCategoryClick = (category: string) => { const element = categoryRefs.current.get(category); if (element) { scrollerRef.current?.scrollIntoViewNode({node: element, shouldScrollToStart: true}); } }; const handleHover = (emoji: Emoji | null) => { setHoveredEmoji(emoji); }; const searchBar = !hideSearchBar ? ( ) : null; return (
{hasPortal && searchBar ? {searchBar} : null}
{!hasPortal && searchBar}
{virtualRows.map((row) => (
{ if (el && 'category' in row) { categoryRefs.current.set(row.category, el); } } : undefined } >
))}
); }, );