// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { createContext, memo, useCallback, useContext, useMemo, useState, } from 'react'; import { strictAssert } from '../../util/assert'; import type { LocalizerType } from '../../types/I18N'; import type { StickerPackType, StickerType } from '../../state/ducks/stickers'; import type { EmojiSkinTone } from './data/emojis'; import { EmojiPickerCategory, type EmojiParentKey } from './data/emojis'; import type { FunGifSelection, GifType } from './panels/FunPanelGifs'; import type { FunGifsSection, FunStickersSection } from './constants'; import { type FunEmojisSection, FunGifsCategory, FunPickerTabKey, FunSectionCommon, FunStickersSectionBase, toFunStickersPackSection, } from './constants'; import type { fetchGifsFeatured, fetchGifsSearch } from './data/gifs'; import type { tenorDownload } from './data/tenor'; import type { FunEmojiSelection } from './panels/FunPanelEmojis'; import type { FunStickerSelection } from './panels/FunPanelStickers'; import { FunEmojiLocalizationProvider } from './FunEmojiLocalizationProvider'; export type FunContextSmartProps = Readonly<{ i18n: LocalizerType; // Recents recentEmojis: ReadonlyArray; recentStickers: ReadonlyArray; recentGifs: ReadonlyArray; // Emojis emojiSkinToneDefault: EmojiSkinTone | null; onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void; onOpenCustomizePreferredReactionsModal: () => void; onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; // Stickers installedStickerPacks: ReadonlyArray; showStickerPickerHint: boolean; onClearStickerPickerHint: () => unknown; onSelectSticker: (stickerSelection: FunStickerSelection) => void; // GIFs fetchGifsFeatured: typeof fetchGifsFeatured; fetchGifsSearch: typeof fetchGifsSearch; fetchGif: typeof tenorDownload; onSelectGif: (gifSelection: FunGifSelection) => void; }>; export type FunContextProps = FunContextSmartProps & Readonly<{ // Open state onOpenChange: (open: boolean) => void; // Current Tab tab: FunPickerTabKey; onChangeTab: (key: FunPickerTabKey) => unknown; // Search searchInput: string; onSearchInputChange: (nextSearchInput: string) => void; shouldAutoFocus: boolean; onChangeShouldAutoFocus: (shouldAutoFocus: boolean) => void; // Current Section selectedEmojisSection: FunEmojisSection; selectedStickersSection: FunStickersSection; selectedGifsSection: FunGifsSection; onChangeSelectedEmojisSection: (section: FunEmojisSection) => void; onChangeSelectedStickersSection: (section: FunStickersSection) => void; onChangeSelectedSelectGifsSection: (section: FunGifsSection) => void; }>; export const FunContext = createContext(null); export function useFunContext(): FunContextProps { const fun = useContext(FunContext); strictAssert(fun != null, 'Must be wrapped with '); return fun; } type FunProviderInnerProps = FunContextProps & { children: React.ReactNode; }; const FunProviderInner = memo(function FunProviderInner( props: FunProviderInnerProps ): JSX.Element { return ( {props.children} ); }); export type FunProviderProps = FunContextSmartProps & { children: React.ReactNode; }; export const FunProvider = memo(function FunProvider( props: FunProviderProps ): JSX.Element { // Current Tab const [tab, setTab] = useState(FunPickerTabKey.Emoji); const handleChangeTab = useCallback((key: FunPickerTabKey) => { setTab(key); }, []); // Search Input const [searchInput, setSearchInput] = useState(''); const searchQuery = useMemo(() => searchInput.trim(), [searchInput]); const handleSearchInputChange = useCallback((newSearchInput: string) => { setSearchInput(newSearchInput); }, []); const [shouldAutoFocus, setShouldAutoFocus] = useState(true); const handleChangeShouldAutofocus = useCallback( (nextShouldAutoFocus: boolean) => { setShouldAutoFocus(nextShouldAutoFocus); }, [] ); const defaultEmojiSection = useMemo((): FunEmojisSection => { if (props.recentEmojis.length) { return FunSectionCommon.Recents; } return EmojiPickerCategory.SmileysAndPeople; }, [props.recentEmojis]); const defaultStickerSection = useMemo((): FunStickersSection => { if (props.recentStickers.length > 0) { return FunSectionCommon.Recents; } const firstInstalledStickerPack = props.installedStickerPacks.at(0); if (firstInstalledStickerPack != null) { return toFunStickersPackSection(firstInstalledStickerPack); } return FunStickersSectionBase.StickersSetup; }, [props.recentStickers, props.installedStickerPacks]); const defaultGifsSection = useMemo((): FunGifsSection => { return FunGifsCategory.Trending; }, []); // Selected Sections const [selectedEmojisSection, setSelectedEmojisSection] = useState( (): FunEmojisSection => { if (searchQuery !== '') { return FunSectionCommon.SearchResults; } return defaultEmojiSection; } ); const [selectedStickersSection, setSelectedStickersSection] = useState( (): FunStickersSection => { if (searchQuery !== '') { return FunSectionCommon.SearchResults; } return defaultStickerSection; } ); const [selectedGifsSection, setSelectedGifsSection] = useState( (): FunGifsSection => { if (searchQuery !== '') { return FunSectionCommon.SearchResults; } return defaultGifsSection; } ); const handleChangeSelectedEmojisSection = useCallback( (section: FunEmojisSection) => { setSelectedEmojisSection(section); }, [] ); const handleChangeSelectedStickersSection = useCallback( (section: FunStickersSection) => { setSelectedStickersSection(section); }, [] ); const handleChangeSelectedGifsSection = useCallback( (section: FunGifsSection) => { setSelectedGifsSection(section); }, [] ); const handleOpenChange = useCallback( (open: boolean) => { if (open) { return; } setSearchInput(''); setSelectedEmojisSection(defaultEmojiSection); setSelectedStickersSection(defaultStickerSection); setSelectedGifsSection(defaultGifsSection); setShouldAutoFocus(true); }, [defaultEmojiSection, defaultStickerSection, defaultGifsSection] ); return ( {props.children} ); });