This commit is contained in:
vineyardbovines
2026-04-14 08:21:41 -04:00
parent 808ebd1379
commit ef3ad06487
11 changed files with 569 additions and 347 deletions
-344
View File
@@ -1,344 +0,0 @@
import {
useCallback,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react'
import {type TextInput, View} from 'react-native'
import {useWindowDimensions} from 'react-native'
import {Image} from 'expo-image'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {cleanError} from '#/lib/strings/errors'
import {
useFeaturedGifsQuery as useKlipyFeaturedGifsQuery,
useGifSearchQuery as useKlipyGifSearchQuery,
} from '#/state/queries/klipy'
import {
type Gif,
gifPreviewUrl,
useTenorFeaturedGifsQuery,
useTenorGifSearchQuery,
} from '#/state/queries/tenor'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {type ListMethods} from '#/view/com/util/List'
import {atoms as a, ios, native, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
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 {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
export function GifSelectDialog({
controlRef,
onClose,
onSelectGif: onSelectGifProp,
}: {
controlRef: React.RefObject<{open: () => void} | null>
onClose?: () => void
onSelectGif: (gif: Gif) => void
}) {
const control = Dialog.useDialogControl()
useImperativeHandle(controlRef, () => ({
open: () => control.open(),
}))
const onSelectGif = useCallback(
(gif: Gif) => {
control.close(() => onSelectGifProp(gif))
},
[control, onSelectGifProp],
)
const renderErrorBoundary = useCallback(
(error: any) => <DialogError details={String(error)} />,
[],
)
return (
<Dialog.Outer
control={control}
onClose={onClose}
nativeOptions={{
bottomInset: 0,
// use system corner radius on iOS
...ios({cornerRadius: undefined}),
fullHeight: true,
}}>
<Dialog.Handle />
<ErrorBoundary renderError={renderErrorBoundary}>
<GifList control={control} onSelectGif={onSelectGif} />
</ErrorBoundary>
</Dialog.Outer>
)
}
function GifList({
control,
onSelectGif,
}: {
control: Dialog.DialogControlProps
onSelectGif: (gif: Gif) => void
}) {
const ax = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
const textInputRef = useRef<TextInput>(null)
const listRef = useRef<ListMethods>(null)
const [undeferredSearch, setSearch] = useState('')
const search = useThrottledValue(undeferredSearch, 500)
const {height} = useWindowDimensions()
const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable)
const isSearching = search.length > 0
const klipyTrending = useKlipyFeaturedGifsQuery({enabled: useKlipy})
const klipySearch = useKlipyGifSearchQuery(search, {enabled: useKlipy})
const tenorTrending = useTenorFeaturedGifsQuery({enabled: !useKlipy})
const tenorSearch = useTenorGifSearchQuery(search, {enabled: !useKlipy})
const {
data,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
error,
isPending,
isError,
refetch,
} = useKlipy
? isSearching
? klipySearch
: klipyTrending
: isSearching
? tenorSearch
: tenorTrending
const flattenedData = useMemo(() => {
return data?.pages.flatMap(page => page.results) || []
}, [data])
const renderItem = useCallback(
({item}: {item: Gif}) => {
return <GifPreview gif={item} onSelectGif={onSelectGif} />
},
[onSelectGif],
)
const onEndReached = 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 (
<View
style={[
native(a.pt_4xl),
a.relative,
a.mb_lg,
a.flex_row,
a.align_center,
!gtMobile && web(a.gap_md),
a.pb_sm,
t.atoms.bg,
]}>
{!gtMobile && IS_WEB && (
<Button
size="small"
variant="ghost"
color="secondary"
shape="round"
onPress={() => control.close()}
label={_(msg`Close GIF dialog`)}>
<ButtonIcon icon={Arrow} size="md" />
</Button>
)}
<TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}>
<TextField.Icon icon={Search} />
<TextField.Input
label={_(msg`Search GIFs`)}
placeholder={useKlipy ? _(msg`Search KLIPY`) : _(msg`Search Tenor`)}
onChangeText={text => {
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()
}
}}
/>
</TextField.Root>
</View>
)
}, [gtMobile, t.atoms.bg, _, control, useKlipy])
return (
<>
{gtMobile && <Dialog.Close />}
<Dialog.InnerFlatList
ref={listRef}
key={gtMobile ? '3 cols' : '2 cols'}
data={flattenedData}
renderItem={renderItem}
numColumns={gtMobile ? 3 : 2}
columnWrapperStyle={[a.gap_sm]}
contentContainerStyle={[native([a.px_xl, {minHeight: height}])]}
webInnerStyle={[web({minHeight: '80vh'})]}
webInnerContentContainerStyle={[web(a.pb_0)]}
ListHeaderComponent={
<>
{listHeader}
{!hasData && (
<ListMaybePlaceholder
isLoading={isPending}
isError={isError}
onRetry={refetch}
onGoBack={onGoBack}
emptyType="results"
sideBorders={false}
topBorder={false}
errorTitle={_(msg`Failed to load GIFs`)}
errorMessage={
useKlipy
? _(msg`There was an issue connecting to KLIPY.`)
: _(msg`There was an issue connecting to Tenor.`)
}
emptyMessage={
isSearching
? _(msg`No search results found for "${search}".`)
: useKlipy
? _(
msg`No featured GIFs found. There may be an issue with KLIPY.`,
)
: _(
msg`No featured GIFs found. There may be an issue with Tenor.`,
)
}
/>
)}
</>
}
stickyHeaderIndices={[0]}
onEndReached={onEndReached}
onEndReachedThreshold={4}
keyExtractor={(item: Gif) => item.id}
keyboardDismissMode="on-drag"
ListFooterComponent={
hasData ? (
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
style={{borderTopWidth: 0}}
/>
) : null
}
/>
</>
)
}
function DialogError({details}: {details?: string}) {
const {_} = useLingui()
const control = Dialog.useDialogContext()
return (
<Dialog.ScrollableInner
style={a.gap_md}
label={_(msg`An error has occurred`)}>
<Dialog.Close />
<ErrorScreen
title={_(msg`Oh no!`)}
message={_(
msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
)}
details={details}
/>
<Button
label={_(msg`Close dialog`)}
onPress={() => control.close()}
color="primary"
size="large"
variant="solid">
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
</Dialog.ScrollableInner>
)
}
export function GifPreview({
gif,
onSelectGif,
}: {
gif: Gif
onSelectGif: (gif: Gif) => void
}) {
const ax = useAnalytics()
const {gtTablet} = useBreakpoints()
const {_} = useLingui()
const t = useTheme()
const onPress = useCallback(() => {
ax.metric('composer:gif:select', {})
onSelectGif(gif)
}, [ax, onSelectGif, gif])
return (
<Button
label={_(msg`Select GIF "${gif.title}"`)}
style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]}
onPress={onPress}>
{({pressed}) => (
<Image
style={[
a.flex_1,
a.mb_sm,
a.rounded_sm,
a.aspect_square,
{opacity: pressed ? 0.8 : 1},
t.atoms.bg_contrast_25,
]}
source={{
uri: gifPreviewUrl(gif.media_formats.tinygif.url),
}}
contentFit="cover"
accessibilityLabel={gif.title}
accessibilityHint=""
cachePolicy="none"
accessibilityIgnoresInvertColors
/>
)}
</Button>
)
}
+140
View File
@@ -0,0 +1,140 @@
import {useImperativeHandle, useRef, useState} from 'react'
import {type TextInput} from 'react-native'
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
import {type ListMethods} from '#/view/com/util/List'
import {ios, useBreakpoints} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
import {GifPickerErrorBoundary} from '#/features/gifPicker/components/GifPickerErrorBoundary'
import {GifPickerGrid} from '#/features/gifPicker/components/GifPickerGrid'
import {GifPickerHeader} from '#/features/gifPicker/components/GifPickerHeader'
import {GifPickerPlaceholder} from '#/features/gifPicker/components/GifPickerPlaceholder'
import {useGifPickerData} from '#/features/gifPicker/hooks/useGifPickerData'
import {type Gif} from '#/features/gifPicker/types'
export function GifPickerDialog({
controlRef,
onClose,
onSelectGif: onSelectGifProp,
}: {
controlRef: React.RefObject<{open: () => void} | null>
onClose?: () => void
onSelectGif: (gif: Gif) => void
}) {
const control = Dialog.useDialogControl()
useImperativeHandle(controlRef, () => ({
open: () => control.open(),
}))
const onSelectGif = (gif: Gif) => {
control.close(() => onSelectGifProp(gif))
}
return (
<Dialog.Outer
control={control}
onClose={onClose}
nativeOptions={{
bottomInset: 0,
// use system corner radius on iOS
...ios({cornerRadius: undefined}),
fullHeight: true,
}}>
<Dialog.Handle />
<ErrorBoundary
renderError={error => (
<GifPickerErrorBoundary details={String(error)} />
)}>
<GifPickerBody control={control} onSelectGif={onSelectGif} />
</ErrorBoundary>
</Dialog.Outer>
)
}
function GifPickerBody({
control,
onSelectGif,
}: {
control: Dialog.DialogControlProps
onSelectGif: (gif: Gif) => void
}) {
const {gtMobile} = useBreakpoints()
const textInputRef = useRef<TextInput>(null)
const listRef = useRef<ListMethods>(null)
const [rawSearch, setRawSearch] = useState('')
const search = useThrottledValue(rawSearch, 500)
const {
data,
fetchNextPage,
isFetchingNextPage,
hasNextPage,
error,
isPending,
isError,
isSearching,
refetch,
} = useGifPickerData(search)
const items = data?.pages.flatMap(page => page.results) ?? []
const hasData = items.length > 0
const onEndReached = () => {
if (isFetchingNextPage || !hasNextPage || error) return
void fetchNextPage()
}
const onGoBack = () => {
if (isSearching) {
textInputRef.current?.clear()
setRawSearch('')
} else {
control.close()
}
}
const onChangeSearch = (text: string) => {
setRawSearch(text)
listRef.current?.scrollToOffset({offset: 0, animated: false})
}
const header = (
<>
<GifPickerHeader
inputRef={textInputRef}
onChangeText={onChangeSearch}
onClose={() => control.close()}
onEscape={() => control.close()}
/>
{!hasData && (
<GifPickerPlaceholder
isLoading={isPending}
isError={isError}
isSearching={isSearching}
query={search}
onRetry={refetch}
onGoBack={onGoBack}
/>
)}
</>
)
return (
<>
{gtMobile && <Dialog.Close />}
<GifPickerGrid
ref={listRef}
items={items}
header={header}
hasData={hasData}
isFetchingNextPage={isFetchingNextPage}
error={error}
fetchNextPage={fetchNextPage}
onEndReached={onEndReached}
onSelectGif={onSelectGif}
/>
</>
)
}
+37
View File
@@ -0,0 +1,37 @@
# GIF Picker
Feature module for the GIF selection dialog used by the composer.
## Structure
- `GifPickerDialog.tsx` — entry component. Wraps `Dialog.Outer` with an error
boundary and exposes an imperative `open()` via `controlRef`.
- `components/GifPickerHeader.tsx` — sticky search row. Reserves a slot beneath
the search input where future tab bars (Trending / Recents / Categories) will
render.
- `components/GifPickerGrid.tsx` — column-distribution masonry grid built on
`Dialog.InnerFlatList`. Uses each GIF's intrinsic width/height instead of
forcing a square aspect ratio.
- `components/GifPickerItem.tsx` — single tile. Natural aspect ratio, press
feedback, fires the `composer:gif:select` analytics event.
- `components/GifPickerPlaceholder.tsx` — loading / empty / error states.
- `components/GifPickerErrorBoundary.tsx` — fallback rendered when the data
layer throws.
- `hooks/useGifPickerData.ts` — collapses the Klipy/Tenor provider flag and the
search-vs-featured branching into one hook so the UI never sees both paths.
## Provider switching
Provider selection is gated by the `KlipyGifProviderEnable` analytics feature
flag. Both provider modules in `src/state/queries/{klipy,tenor}.ts` are live
until the Klipy rollout (and the tango backend-proxy PR) is complete. Do not
remove the Tenor path without first confirming Klipy is globally enabled.
## Out of scope (future tickets)
- Autocomplete / autosuggest
- Recents
- Trending tags / categories browsing
- Favorites
- Alt-text-at-pick flow (alt text is still added via a separate dialog after
selection)
@@ -0,0 +1,38 @@
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
export function GifPickerErrorBoundary({details}: {details?: string}) {
const {_} = useLingui()
const control = Dialog.useDialogContext()
return (
<Dialog.ScrollableInner
style={a.gap_md}
label={_(msg`An error has occurred`)}>
<Dialog.Close />
<ErrorScreen
title={_(msg`Oh no!`)}
message={_(
msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
)}
details={details}
/>
<Button
label={_(msg`Close dialog`)}
onPress={() => control.close()}
color="primary"
size="large"
variant="solid">
<ButtonText>
<Trans>Close</Trans>
</ButtonText>
</Button>
</Dialog.ScrollableInner>
)
}
@@ -0,0 +1,121 @@
import {forwardRef, useMemo} from 'react'
import {useWindowDimensions, View} from 'react-native'
import {cleanError} from '#/lib/strings/errors'
import {type ListMethods} from '#/view/com/util/List'
import {atoms as a, native, useBreakpoints, web} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {ListFooter} from '#/components/Lists'
import {GifPickerItem} from '#/features/gifPicker/components/GifPickerItem'
import {type Gif} from '#/features/gifPicker/types'
type Props = {
items: Gif[]
header: React.ReactNode
hasData: boolean
isFetchingNextPage: boolean
error: unknown
fetchNextPage: () => Promise<unknown>
onEndReached: () => void
onSelectGif: (gif: Gif) => void
}
export const GifPickerGrid = forwardRef<ListMethods, Props>(
function GifPickerGrid(
{
items,
header,
hasData,
isFetchingNextPage,
error,
fetchNextPage,
onEndReached,
onSelectGif,
},
ref,
) {
const {gtMobile} = useBreakpoints()
const {height} = useWindowDimensions()
const numColumns = gtMobile ? 3 : 2
const columns = useMemo(
() => distributeIntoColumns(items, numColumns),
[items, numColumns],
)
/**
* The grid is a single FlatList row because the tiles are distributed
* into columns up front for masonry. `onEndReached` still fires against
* the outer FlatList's scroll position, so pagination behaves the same
* as a conventional grid.
*/
const data = useMemo(() => (hasData ? [columns] : []), [hasData, columns])
return (
<Dialog.InnerFlatList
ref={ref}
key={String(numColumns)}
data={data}
renderItem={({item}: {item: Gif[][]}) => (
<View style={[a.flex_row, a.gap_sm]}>
{item.map((column, i) => (
<View key={i} style={[a.flex_1, a.gap_sm, {minWidth: 0}]}>
{column.map(gif => (
<GifPickerItem
key={gif.id}
gif={gif}
onSelectGif={onSelectGif}
/>
))}
</View>
))}
</View>
)}
keyExtractor={(_item, index) => `masonry-${index}`}
contentContainerStyle={[native([a.px_xl, {minHeight: height}])]}
webInnerStyle={[web({minHeight: '80vh'})]}
webInnerContentContainerStyle={[web(a.pb_0)]}
ListHeaderComponent={<>{header}</>}
stickyHeaderIndices={[0]}
onEndReached={onEndReached}
onEndReachedThreshold={4}
keyboardDismissMode="on-drag"
ListFooterComponent={
hasData ? (
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
style={{borderTopWidth: 0}}
/>
) : null
}
/>
)
},
)
/**
* Walks `items` in order and pushes each one into the currently shortest
* column, tracking accumulated height-per-unit-width from each GIF's
* intrinsic aspect ratio. Preserves ordering top-to-bottom within each
* column, which keeps pagination behavior intuitive as new pages stream in.
*/
function distributeIntoColumns(items: Gif[], numColumns: number): Gif[][] {
const columns: Gif[][] = Array.from({length: numColumns}, () => [])
const heights = new Array(numColumns).fill(0)
for (const item of items) {
const [w, h] = item.media_formats.tinygif.dims
const ratio = w > 0 && h > 0 ? h / w : 1
let shortest = 0
for (let i = 1; i < numColumns; i++) {
if (heights[i] < heights[shortest]) shortest = i
}
columns[shortest].push(item)
heights[shortest] += ratio
}
return columns
}
@@ -0,0 +1,73 @@
import {type Ref} from 'react'
import {type TextInput, View} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import * as TextField from '#/components/forms/TextField'
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
import {IS_WEB} from '#/env'
export function GifPickerHeader({
inputRef,
onChangeText,
onClose,
onEscape,
}: {
inputRef: Ref<TextInput>
onChangeText: (text: string) => void
onClose: () => void
onEscape: () => void
}) {
const {_} = useLingui()
const t = useTheme()
const {gtMobile} = useBreakpoints()
return (
<View
style={[
native(a.pt_4xl),
a.relative,
a.mb_lg,
a.flex_row,
a.align_center,
!gtMobile && web(a.gap_md),
a.pb_sm,
t.atoms.bg,
]}>
{!gtMobile && IS_WEB && (
<Button
size="small"
variant="ghost"
color="secondary"
shape="round"
onPress={onClose}
label={_(msg`Close GIF dialog`)}>
<ButtonIcon icon={Arrow} size="md" />
</Button>
)}
<TextField.Root style={[!gtMobile && IS_WEB && a.flex_1]}>
<TextField.Icon icon={Search} />
<TextField.Input
label={_(msg`Search GIFs`)}
placeholder={_(msg`Search GIFs`)}
onChangeText={onChangeText}
returnKeyType="search"
clearButtonMode="while-editing"
inputRef={inputRef}
maxLength={50}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Escape') {
onEscape()
}
}}
/>
</TextField.Root>
{/* future: tabs (Trending / Recents / Categories) render here */}
</View>
)
}
@@ -0,0 +1,57 @@
import {Image} from 'expo-image'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {gifPreviewUrl} from '#/state/queries/tenor'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {useAnalytics} from '#/analytics'
import {type Gif} from '#/features/gifPicker/types'
export function GifPickerItem({
gif,
onSelectGif,
}: {
gif: Gif
onSelectGif: (gif: Gif) => void
}) {
const ax = useAnalytics()
const {_} = useLingui()
const t = useTheme()
const [width, height] = gif.media_formats.tinygif.dims
const aspectRatio = width > 0 && height > 0 ? width / height : 1
const onPress = () => {
ax.metric('composer:gif:select', {})
onSelectGif(gif)
}
return (
<Button
label={_(msg`Select GIF "${gif.title}"`)}
onPress={onPress}
style={a.w_full}>
{({pressed}) => (
<Image
style={[
a.w_full,
a.rounded_sm,
t.atoms.bg_contrast_25,
{
aspectRatio,
opacity: pressed ? 0.85 : 1,
transform: [{scale: pressed ? 0.97 : 1}],
},
]}
source={{uri: gifPreviewUrl(gif.media_formats.tinygif.url)}}
contentFit="cover"
accessibilityLabel={gif.title}
accessibilityHint=""
cachePolicy="none"
accessibilityIgnoresInvertColors
/>
)}
</Button>
)
}
@@ -0,0 +1,43 @@
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {ListMaybePlaceholder} from '#/components/Lists'
export function GifPickerPlaceholder({
isLoading,
isError,
isSearching,
query,
onRetry,
onGoBack,
}: {
isLoading: boolean
isError: boolean
isSearching: boolean
query: string
onRetry: () => Promise<unknown>
onGoBack: () => void
}) {
const {_} = useLingui()
return (
<ListMaybePlaceholder
isLoading={isLoading}
isError={isError}
onRetry={onRetry}
onGoBack={onGoBack}
emptyType="results"
sideBorders={false}
topBorder={false}
errorTitle={_(msg`Couldn't load GIFs`)}
errorMessage={_(
msg`There was a problem loading GIFs. Check your connection and try again.`,
)}
emptyMessage={
isSearching
? _(msg`No GIFs found for "${query}".`)
: _(msg`No GIFs to show right now. Try again in a moment.`)
}
/>
)
}
@@ -0,0 +1,54 @@
import {
useFeaturedGifsQuery as useKlipyFeaturedGifsQuery,
useGifSearchQuery as useKlipyGifSearchQuery,
} from '#/state/queries/klipy'
import {
useTenorFeaturedGifsQuery,
useTenorGifSearchQuery,
} from '#/state/queries/tenor'
import {useAnalytics} from '#/analytics'
import {type GifPickerProvider} from '#/features/gifPicker/types'
/**
* Single entry point for the GIF picker's data layer. Wraps the Klipy/Tenor
* feature-flag split and the featured-vs-search branching so the UI only ever
* consumes one query result.
*
* The Tenor path is kept alive until the Klipy rollout is complete.
*/
export function useGifPickerData(
query: string,
{enabled = true}: {enabled?: boolean} = {},
) {
const ax = useAnalytics()
const useKlipy = ax.features.enabled(ax.features.KlipyGifProviderEnable)
const isSearching = query.length > 0
const provider: GifPickerProvider = useKlipy ? 'klipy' : 'tenor'
const klipyFeatured = useKlipyFeaturedGifsQuery({
enabled: enabled && useKlipy && !isSearching,
})
const klipySearch = useKlipyGifSearchQuery(query, {
enabled: enabled && useKlipy && isSearching,
})
const tenorFeatured = useTenorFeaturedGifsQuery({
enabled: enabled && !useKlipy && !isSearching,
})
const tenorSearch = useTenorGifSearchQuery(query, {
enabled: enabled && !useKlipy && isSearching,
})
const active = useKlipy
? isSearching
? klipySearch
: klipyFeatured
: isSearching
? tenorSearch
: tenorFeatured
return {
...active,
provider,
isSearching,
}
}
+3
View File
@@ -0,0 +1,3 @@
export {type Gif} from '#/state/queries/tenor'
export type GifPickerProvider = 'klipy' | 'tenor'
@@ -3,12 +3,12 @@ import {Keyboard} from 'react-native'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {type Gif} from '#/state/queries/tenor'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {GifSelectDialog} from '#/components/dialogs/GifSelect'
import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif'
import {useAnalytics} from '#/analytics'
import {GifPickerDialog} from '#/features/gifPicker/GifPickerDialog'
import {type Gif} from '#/features/gifPicker/types'
type Props = {
onClose?: () => void
@@ -43,7 +43,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {
<GifIcon size="lg" style={disabled && t.atoms.text_contrast_low} />
</Button>
<GifSelectDialog
<GifPickerDialog
controlRef={ref}
onClose={onClose}
onSelectGif={onSelectGif}