mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
fork

Configure Feed

Select the types of activity you want to include in your feed.

at remove-hackfix 318 lines 8.9 kB view raw
1import { 2 useCallback, 3 useImperativeHandle, 4 useMemo, 5 useRef, 6 useState, 7} from 'react' 8import {type TextInput, View} from 'react-native' 9import {useWindowDimensions} from 'react-native' 10import {Image} from 'expo-image' 11import {msg, Trans} from '@lingui/macro' 12import {useLingui} from '@lingui/react' 13 14import {logEvent} from '#/lib/statsig/statsig' 15import {cleanError} from '#/lib/strings/errors' 16import {isWeb} from '#/platform/detection' 17import { 18 type Gif, 19 tenorUrlToBskyGifUrl, 20 useFeaturedGifsQuery, 21 useGifSearchQuery, 22} from '#/state/queries/tenor' 23import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' 24import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 25import {type ListMethods} from '#/view/com/util/List' 26import {atoms as a, ios, native, useBreakpoints, useTheme, web} from '#/alf' 27import {Button, ButtonIcon, ButtonText} from '#/components/Button' 28import * as Dialog from '#/components/Dialog' 29import * as TextField from '#/components/forms/TextField' 30import {useThrottledValue} from '#/components/hooks/useThrottledValue' 31import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' 32import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' 33import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 34 35export function GifSelectDialog({ 36 controlRef, 37 onClose, 38 onSelectGif: onSelectGifProp, 39}: { 40 controlRef: React.RefObject<{open: () => void} | null> 41 onClose?: () => void 42 onSelectGif: (gif: Gif) => void 43}) { 44 const control = Dialog.useDialogControl() 45 46 useImperativeHandle(controlRef, () => ({ 47 open: () => control.open(), 48 })) 49 50 const onSelectGif = useCallback( 51 (gif: Gif) => { 52 control.close(() => onSelectGifProp(gif)) 53 }, 54 [control, onSelectGifProp], 55 ) 56 57 const renderErrorBoundary = useCallback( 58 (error: any) => <DialogError details={String(error)} />, 59 [], 60 ) 61 62 return ( 63 <Dialog.Outer 64 control={control} 65 onClose={onClose} 66 nativeOptions={{ 67 bottomInset: 0, 68 // use system corner radius on iOS 69 ...ios({cornerRadius: undefined}), 70 }}> 71 <Dialog.Handle /> 72 <ErrorBoundary renderError={renderErrorBoundary}> 73 <GifList control={control} onSelectGif={onSelectGif} /> 74 </ErrorBoundary> 75 </Dialog.Outer> 76 ) 77} 78 79function GifList({ 80 control, 81 onSelectGif, 82}: { 83 control: Dialog.DialogControlProps 84 onSelectGif: (gif: Gif) => void 85}) { 86 const {_} = useLingui() 87 const t = useTheme() 88 const {gtMobile} = useBreakpoints() 89 const textInputRef = useRef<TextInput>(null) 90 const listRef = useRef<ListMethods>(null) 91 const [undeferredSearch, setSearch] = useState('') 92 const search = useThrottledValue(undeferredSearch, 500) 93 const {height} = useWindowDimensions() 94 95 const isSearching = search.length > 0 96 97 const trendingQuery = useFeaturedGifsQuery() 98 const searchQuery = useGifSearchQuery(search) 99 100 const { 101 data, 102 fetchNextPage, 103 isFetchingNextPage, 104 hasNextPage, 105 error, 106 isPending, 107 isError, 108 refetch, 109 } = isSearching ? searchQuery : trendingQuery 110 111 const flattenedData = useMemo(() => { 112 return data?.pages.flatMap(page => page.results) || [] 113 }, [data]) 114 115 const renderItem = useCallback( 116 ({item}: {item: Gif}) => { 117 return <GifPreview gif={item} onSelectGif={onSelectGif} /> 118 }, 119 [onSelectGif], 120 ) 121 122 const onEndReached = useCallback(() => { 123 if (isFetchingNextPage || !hasNextPage || error) return 124 fetchNextPage() 125 }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) 126 127 const hasData = flattenedData.length > 0 128 129 const onGoBack = useCallback(() => { 130 if (isSearching) { 131 // clear the input and reset the state 132 textInputRef.current?.clear() 133 setSearch('') 134 } else { 135 control.close() 136 } 137 }, [control, isSearching]) 138 139 const listHeader = useMemo(() => { 140 return ( 141 <View 142 style={[ 143 native(a.pt_4xl), 144 a.relative, 145 a.mb_lg, 146 a.flex_row, 147 a.align_center, 148 !gtMobile && web(a.gap_md), 149 a.pb_sm, 150 t.atoms.bg, 151 ]}> 152 {!gtMobile && isWeb && ( 153 <Button 154 size="small" 155 variant="ghost" 156 color="secondary" 157 shape="round" 158 onPress={() => control.close()} 159 label={_(msg`Close GIF dialog`)}> 160 <ButtonIcon icon={Arrow} size="md" /> 161 </Button> 162 )} 163 164 <TextField.Root style={[!gtMobile && isWeb && a.flex_1]}> 165 <TextField.Icon icon={Search} /> 166 <TextField.Input 167 label={_(msg`Search GIFs`)} 168 placeholder={_(msg`Search Tenor`)} 169 onChangeText={text => { 170 setSearch(text) 171 listRef.current?.scrollToOffset({offset: 0, animated: false}) 172 }} 173 returnKeyType="search" 174 clearButtonMode="while-editing" 175 inputRef={textInputRef} 176 maxLength={50} 177 onKeyPress={({nativeEvent}) => { 178 if (nativeEvent.key === 'Escape') { 179 control.close() 180 } 181 }} 182 /> 183 </TextField.Root> 184 </View> 185 ) 186 }, [gtMobile, t.atoms.bg, _, control]) 187 188 return ( 189 <> 190 {gtMobile && <Dialog.Close />} 191 <Dialog.InnerFlatList 192 ref={listRef} 193 key={gtMobile ? '3 cols' : '2 cols'} 194 data={flattenedData} 195 renderItem={renderItem} 196 numColumns={gtMobile ? 3 : 2} 197 columnWrapperStyle={[a.gap_sm]} 198 contentContainerStyle={[native([a.px_xl, {minHeight: height}])]} 199 webInnerStyle={[web({minHeight: '80vh'})]} 200 webInnerContentContainerStyle={[web(a.pb_0)]} 201 ListHeaderComponent={ 202 <> 203 {listHeader} 204 {!hasData && ( 205 <ListMaybePlaceholder 206 isLoading={isPending} 207 isError={isError} 208 onRetry={refetch} 209 onGoBack={onGoBack} 210 emptyType="results" 211 sideBorders={false} 212 topBorder={false} 213 errorTitle={_(msg`Failed to load GIFs`)} 214 errorMessage={_(msg`There was an issue connecting to Tenor.`)} 215 emptyMessage={ 216 isSearching 217 ? _(msg`No search results found for "${search}".`) 218 : _( 219 msg`No featured GIFs found. There may be an issue with Tenor.`, 220 ) 221 } 222 /> 223 )} 224 </> 225 } 226 stickyHeaderIndices={[0]} 227 onEndReached={onEndReached} 228 onEndReachedThreshold={4} 229 keyExtractor={(item: Gif) => item.id} 230 keyboardDismissMode="on-drag" 231 ListFooterComponent={ 232 hasData ? ( 233 <ListFooter 234 isFetchingNextPage={isFetchingNextPage} 235 error={cleanError(error)} 236 onRetry={fetchNextPage} 237 style={{borderTopWidth: 0}} 238 /> 239 ) : null 240 } 241 /> 242 </> 243 ) 244} 245 246function DialogError({details}: {details?: string}) { 247 const {_} = useLingui() 248 const control = Dialog.useDialogContext() 249 250 return ( 251 <Dialog.ScrollableInner 252 style={a.gap_md} 253 label={_(msg`An error has occurred`)}> 254 <Dialog.Close /> 255 <ErrorScreen 256 title={_(msg`Oh no!`)} 257 message={_( 258 msg`There was an unexpected issue in the application. Please let us know if this happened to you!`, 259 )} 260 details={details} 261 /> 262 <Button 263 label={_(msg`Close dialog`)} 264 onPress={() => control.close()} 265 color="primary" 266 size="large" 267 variant="solid"> 268 <ButtonText> 269 <Trans>Close</Trans> 270 </ButtonText> 271 </Button> 272 </Dialog.ScrollableInner> 273 ) 274} 275 276export function GifPreview({ 277 gif, 278 onSelectGif, 279}: { 280 gif: Gif 281 onSelectGif: (gif: Gif) => void 282}) { 283 const {gtTablet} = useBreakpoints() 284 const {_} = useLingui() 285 const t = useTheme() 286 287 const onPress = useCallback(() => { 288 logEvent('composer:gif:select', {}) 289 onSelectGif(gif) 290 }, [onSelectGif, gif]) 291 292 return ( 293 <Button 294 label={_(msg`Select GIF "${gif.title}"`)} 295 style={[a.flex_1, gtTablet ? {maxWidth: '33%'} : {maxWidth: '50%'}]} 296 onPress={onPress}> 297 {({pressed}) => ( 298 <Image 299 style={[ 300 a.flex_1, 301 a.mb_sm, 302 a.rounded_sm, 303 {aspectRatio: 1, opacity: pressed ? 0.8 : 1}, 304 t.atoms.bg_contrast_25, 305 ]} 306 source={{ 307 uri: tenorUrlToBskyGifUrl(gif.media_formats.tinygif.url), 308 }} 309 contentFit="cover" 310 accessibilityLabel={gif.title} 311 accessibilityHint="" 312 cachePolicy="none" 313 accessibilityIgnoresInvertColors 314 /> 315 )} 316 </Button> 317 ) 318}