import React, {useCallback, useMemo, useRef, useState} from 'react' import {Keyboard, TextInput, View} from 'react-native' import {Image} from 'expo-image' import {BottomSheetFlatListMethods} from '@discord/bottom-sheet' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {GIPHY_PRIVACY_POLICY} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' import {cleanError} from '#/lib/strings/errors' import {isWeb} from '#/platform/detection' import { useExternalEmbedsPrefs, useSetExternalEmbedPref, } from '#/state/preferences' import {Gif, useGifphySearch, useGiphyTrending} from '#/state/queries/giphy' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import * as Dialog from '#/components/Dialog' import * as TextField from '#/components/forms/TextField' import {useThrottledValue} from '#/components/hooks/useThrottledValue' import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' import {InlineLinkText} from '#/components/Link' import {Button, ButtonIcon, ButtonText} from '../Button' import {ListFooter, ListMaybePlaceholder} from '../Lists' import {Text} from '../Typography' export function GifSelectDialog({ control, onClose, onSelectGif: onSelectGifProp, }: { control: Dialog.DialogControlProps onClose: () => void onSelectGif: (gif: Gif) => void }) { const externalEmbedsPrefs = useExternalEmbedsPrefs() const onSelectGif = useCallback( (gif: Gif) => { control.close(() => onSelectGifProp(gif)) }, [control, onSelectGifProp], ) let content = null let snapPoints switch (externalEmbedsPrefs?.giphy) { case 'show': content = snapPoints = ['100%'] break case 'hide': default: content = break } const renderErrorBoundary = useCallback( (error: any) => , [], ) return ( {content} ) } function GifList({ control, onSelectGif, }: { control: Dialog.DialogControlProps onSelectGif: (gif: Gif) => void }) { const {_} = useLingui() const t = useTheme() const {gtMobile} = useBreakpoints() const textInputRef = useRef(null) const listRef = useRef(null) const [undeferredSearch, setSearch] = useState('') const search = useThrottledValue(undeferredSearch, 500) const isSearching = search.length > 0 const trendingQuery = useGiphyTrending() const searchQuery = useGifphySearch(search) const { data, fetchNextPage, isFetchingNextPage, hasNextPage, error, isLoading, isError, refetch, } = isSearching ? searchQuery : trendingQuery const flattenedData = useMemo(() => { const uniquenessSet = new Set() function filter(gif: Gif) { if (!gif) return false if (uniquenessSet.has(gif.id)) { return false } uniquenessSet.add(gif.id) return true } return data?.pages.flatMap(page => page.data.filter(filter)) || [] }, [data]) const renderItem = useCallback( ({item}: {item: Gif}) => { return }, [onSelectGif], ) const onEndReached = React.useCallback(() => { if (isFetchingNextPage || !hasNextPage || error) return fetchNextPage() }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) const hasData = flattenedData.length > 0 const onGoBack = useCallback(() => { if (isSearching) { // clear the input and reset the state textInputRef.current?.clear() setSearch('') } else { control.close() } }, [control, isSearching]) const listHeader = useMemo(() => { return ( {/* cover top corners */} {!gtMobile && isWeb && ( )} { setSearch(text) listRef.current?.scrollToOffset({offset: 0, animated: false}) }} returnKeyType="search" clearButtonMode="while-editing" inputRef={textInputRef} maxLength={50} onKeyPress={({nativeEvent}) => { if (nativeEvent.key === 'Escape') { control.close() } }} /> ) }, [gtMobile, t.atoms.bg, _, control]) return ( <> {gtMobile && } {listHeader} {!hasData && ( )} } stickyHeaderIndices={[0]} onEndReached={onEndReached} onEndReachedThreshold={4} keyExtractor={(item: Gif) => item.id} // @ts-expect-error web only style={isWeb && {minHeight: '100vh'}} onScrollBeginDrag={() => Keyboard.dismiss()} ListFooterComponent={ hasData ? ( ) : null } /> ) } function GifPreview({ gif, onSelectGif, }: { gif: Gif onSelectGif: (gif: Gif) => void }) { const {gtTablet} = useBreakpoints() const {_} = useLingui() const t = useTheme() const onPress = useCallback(() => { logEvent('composer:gif:select', {}) onSelectGif(gif) }, [onSelectGif, gif]) return ( ) } function GiphyConsentPrompt({control}: {control: Dialog.DialogControlProps}) { const {_} = useLingui() const t = useTheme() const {gtMobile} = useBreakpoints() const setExternalEmbedPref = useSetExternalEmbedPref() const onShowPress = useCallback(() => { setExternalEmbedPref('giphy', 'show') }, [setExternalEmbedPref]) const onHidePress = useCallback(() => { setExternalEmbedPref('giphy', 'hide') control.close() }, [control, setExternalEmbedPref]) const gtMobileWeb = gtMobile && isWeb return ( Permission to use GIPHY Bluesky uses GIPHY to provide the GIF selector feature. GIPHY may collect information about you and your device. You can find out more in their{' '} control.close()}> privacy policy . ) } function DialogError({details}: {details?: string}) { const {_} = useLingui() const control = Dialog.useDialogContext() return ( ) }