From da96fb1ef5a37018b6a238c3614e9b845d8e2686 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 4 Jun 2024 04:05:46 +0300 Subject: [PATCH] Native `formSheet` for GIF select on iOS (#4328) * native formsheet for gif select * trigger confirm discard if have gif * give modal a background color * fix web top bar - unrelated but I cba to make a separate PR --- src/components/dialogs/GifSelect.ios.tsx | 255 ++++++++++++++++++ src/components/dialogs/GifSelect.shared.tsx | 53 ++++ src/components/dialogs/GifSelect.tsx | 65 ++--- src/view/com/composer/Composer.tsx | 5 +- src/view/com/composer/photos/SelectGifBtn.tsx | 11 +- 5 files changed, 331 insertions(+), 58 deletions(-) create mode 100644 src/components/dialogs/GifSelect.ios.tsx create mode 100644 src/components/dialogs/GifSelect.shared.tsx diff --git a/src/components/dialogs/GifSelect.ios.tsx b/src/components/dialogs/GifSelect.ios.tsx new file mode 100644 index 00000000..091a23e5 --- /dev/null +++ b/src/components/dialogs/GifSelect.ios.tsx @@ -0,0 +1,255 @@ +import React, { + useCallback, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' +import {Modal, ScrollView, TextInput, View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {cleanError} from '#/lib/strings/errors' +import { + Gif, + useFeaturedGifsQuery, + useGifSearchQuery, +} from '#/state/queries/tenor' +import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' +import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' +import {FlatList_INTERNAL} from '#/view/com/util/Views' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import * as TextField from '#/components/forms/TextField' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' +import {Button, ButtonText} from '../Button' +import {Handle} from '../Dialog' +import {useThrottledValue} from '../hooks/useThrottledValue' +import {ListFooter, ListMaybePlaceholder} from '../Lists' +import {GifPreview} from './GifSelect.shared' + +export function GifSelectDialog({ + controlRef, + onClose, + onSelectGif: onSelectGifProp, +}: { + controlRef: React.RefObject<{open: () => void}> + onClose: () => void + onSelectGif: (gif: Gif) => void +}) { + const t = useTheme() + const [open, setOpen] = useState(false) + + useImperativeHandle(controlRef, () => ({ + open: () => setOpen(true), + })) + + const close = useCallback(() => { + setOpen(false) + onClose() + }, [onClose]) + + const onSelectGif = useCallback( + (gif: Gif) => { + onSelectGifProp(gif) + close() + }, + [onSelectGifProp, close], + ) + + const renderErrorBoundary = useCallback( + (error: any) => , + [close], + ) + + return ( + + + + + + + + + ) +} + +function GifList({ + onSelectGif, +}: { + close: () => void + 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 = useFeaturedGifsQuery() + const searchQuery = useGifSearchQuery(search) + + const { + data, + fetchNextPage, + isFetchingNextPage, + hasNextPage, + error, + isLoading, + isError, + refetch, + } = isSearching ? searchQuery : trendingQuery + + const flattenedData = useMemo(() => { + return data?.pages.flatMap(page => page.results) || [] + }, [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 { + close() + } + }, [isSearching]) + + const listHeader = useMemo(() => { + return ( + + {/* cover top corners */} + + + + + { + setSearch(text) + listRef.current?.scrollToOffset({offset: 0, animated: false}) + }} + returnKeyType="search" + clearButtonMode="while-editing" + inputRef={textInputRef} + maxLength={50} + /> + + + ) + }, [t.atoms.bg, _]) + + return ( + + {listHeader} + {!hasData && ( + + )} + + } + stickyHeaderIndices={[0]} + onEndReached={onEndReached} + onEndReachedThreshold={4} + keyExtractor={(item: Gif) => item.id} + keyboardDismissMode="on-drag" + ListFooterComponent={ + hasData ? ( + + ) : null + } + /> + ) +} + +function ModalError({details, close}: {details?: string; close: () => void}) { + const {_} = useLingui() + + return ( + + + + + ) +} diff --git a/src/components/dialogs/GifSelect.shared.tsx b/src/components/dialogs/GifSelect.shared.tsx new file mode 100644 index 00000000..90b2abaa --- /dev/null +++ b/src/components/dialogs/GifSelect.shared.tsx @@ -0,0 +1,53 @@ +import React, {useCallback} from 'react' +import {Image} from 'expo-image' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {logEvent} from '#/lib/statsig/statsig' +import {Gif} from '#/state/queries/tenor' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button} from '../Button' + +export 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 ( + + ) +} diff --git a/src/components/dialogs/GifSelect.tsx b/src/components/dialogs/GifSelect.tsx index 4a3ce42a..a64edcd6 100644 --- a/src/components/dialogs/GifSelect.tsx +++ b/src/components/dialogs/GifSelect.tsx @@ -1,11 +1,15 @@ -import React, {useCallback, useMemo, useRef, useState} from 'react' +import React, { + useCallback, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react' import {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 {logEvent} from '#/lib/statsig/statsig' import {cleanError} from '#/lib/strings/errors' import {isWeb} from '#/platform/detection' import { @@ -23,16 +27,23 @@ import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arr import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' import {Button, ButtonIcon, ButtonText} from '../Button' import {ListFooter, ListMaybePlaceholder} from '../Lists' +import {GifPreview} from './GifSelect.shared' export function GifSelectDialog({ - control, + controlRef, onClose, onSelectGif: onSelectGifProp, }: { - control: Dialog.DialogControlProps + controlRef: React.RefObject<{open: () => void}> onClose: () => void onSelectGif: (gif: Gif) => void }) { + const control = Dialog.useDialogControl() + + useImperativeHandle(controlRef, () => ({ + open: () => control.open(), + })) + const onSelectGif = useCallback( (gif: Gif) => { control.close(() => onSelectGifProp(gif)) @@ -233,50 +244,6 @@ function GifList({ ) } -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 DialogError({details}: {details?: string}) { const {_} = useLingui() const control = Dialog.useDialogContext() diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index ad79cdb5..93cc87fc 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -173,7 +173,7 @@ export const ComposePost = observer(function ComposePost({ ) const onPressCancel = useCallback(() => { - if (graphemeLength > 0 || !gallery.isEmpty) { + if (graphemeLength > 0 || !gallery.isEmpty || extGif) { closeAllDialogs() if (Keyboard) { Keyboard.dismiss() @@ -183,6 +183,7 @@ export const ComposePost = observer(function ComposePost({ onClose() } }, [ + extGif, graphemeLength, gallery.isEmpty, closeAllDialogs, @@ -728,8 +729,6 @@ function useAnimatedBorders() { const styles = StyleSheet.create({ topbar: {}, topbarDesktop: { - paddingTop: 10, - paddingBottom: 10, height: 50, }, topbarInner: { diff --git a/src/view/com/composer/photos/SelectGifBtn.tsx b/src/view/com/composer/photos/SelectGifBtn.tsx index 60cef9a1..d13df0a1 100644 --- a/src/view/com/composer/photos/SelectGifBtn.tsx +++ b/src/view/com/composer/photos/SelectGifBtn.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react' +import React, {useCallback, useRef} from 'react' import {Keyboard} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -7,7 +7,6 @@ import {logEvent} from '#/lib/statsig/statsig' import {Gif} from '#/state/queries/tenor' import {atoms as a, useTheme} from '#/alf' import {Button} from '#/components/Button' -import {useDialogControl} from '#/components/Dialog' import {GifSelectDialog} from '#/components/dialogs/GifSelect' import {GifSquare_Stroke2_Corner0_Rounded as GifIcon} from '#/components/icons/Gif' @@ -19,14 +18,14 @@ type Props = { export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) { const {_} = useLingui() - const control = useDialogControl() + const ref = useRef<{open: () => void}>(null) const t = useTheme() const onPressSelectGif = useCallback(async () => { logEvent('composer:gif:open', {}) Keyboard.dismiss() - control.open() - }, [control]) + ref.current?.open() + }, []) return ( <> @@ -44,7 +43,7 @@ export function SelectGifBtn({onClose, onSelectGif, disabled}: Props) {