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

Configure Feed

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

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