Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 8f82ad66df8a21c9a0905dcbf882dd87e892ac8f 207 lines 5.7 kB view raw
1import React, {type JSX} from 'react' 2import { 3 ActivityIndicator, 4 FlatList as RNFlatList, 5 RefreshControl, 6 type StyleProp, 7 View, 8 type ViewStyle, 9} from 'react-native' 10import {type AppBskyGraphDefs as GraphDefs} from '@atproto/api' 11import {msg} from '@lingui/macro' 12import {useLingui} from '@lingui/react' 13 14import {usePalette} from '#/lib/hooks/usePalette' 15import {cleanError} from '#/lib/strings/errors' 16import {s} from '#/lib/styles' 17import {logger} from '#/logger' 18import {useModerationOpts} from '#/state/preferences/moderation-opts' 19import {type MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists' 20import {atoms as a, useTheme} from '#/alf' 21import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList' 22import * as ListCard from '#/components/ListCard' 23import {Text} from '#/components/Typography' 24import {ErrorMessage} from '../util/error/ErrorMessage' 25import {List} from '../util/List' 26 27const LOADING = {_reactKey: '__loading__'} 28const EMPTY = {_reactKey: '__empty__'} 29const ERROR_ITEM = {_reactKey: '__error__'} 30 31export function MyLists({ 32 filter, 33 inline, 34 style, 35 renderItem, 36 testID, 37}: { 38 filter: MyListsFilter 39 inline?: boolean 40 style?: StyleProp<ViewStyle> 41 renderItem?: (list: GraphDefs.ListView, index: number) => JSX.Element 42 testID?: string 43}) { 44 const pal = usePalette('default') 45 const t = useTheme() 46 const {_} = useLingui() 47 const moderationOpts = useModerationOpts() 48 const [isPTRing, setIsPTRing] = React.useState(false) 49 const {data, isFetching, isFetched, isError, error, refetch} = 50 useMyListsQuery(filter) 51 const isEmpty = !isFetching && !data?.length 52 53 const items = React.useMemo(() => { 54 let items: any[] = [] 55 if (isError && isEmpty) { 56 items = items.concat([ERROR_ITEM]) 57 } 58 if ((!isFetched && isFetching) || !moderationOpts) { 59 items = items.concat([LOADING]) 60 } else if (isEmpty) { 61 items = items.concat([EMPTY]) 62 } else { 63 items = items.concat(data) 64 } 65 return items 66 }, [isError, isEmpty, isFetched, isFetching, moderationOpts, data]) 67 68 let emptyText 69 switch (filter) { 70 case 'curate': 71 emptyText = _( 72 msg`Lists allow you to see content from your favorite people.`, 73 ) 74 break 75 case 'mod': 76 emptyText = _( 77 msg`Public, sharable lists of users to mute or block in bulk.`, 78 ) 79 break 80 default: 81 emptyText = _(msg`You have no lists.`) 82 break 83 } 84 85 // events 86 // = 87 88 const onRefresh = React.useCallback(async () => { 89 setIsPTRing(true) 90 try { 91 await refetch() 92 } catch (err) { 93 logger.error('Failed to refresh lists', {message: err}) 94 } 95 setIsPTRing(false) 96 }, [refetch, setIsPTRing]) 97 98 // rendering 99 // = 100 101 const renderItemInner = React.useCallback( 102 ({item, index}: {item: any; index: number}) => { 103 if (item === EMPTY) { 104 return ( 105 <View style={[a.flex_1, a.align_center, a.gap_sm, a.px_xl, a.pt_3xl]}> 106 <View 107 style={[ 108 a.align_center, 109 a.justify_center, 110 a.rounded_full, 111 { 112 width: 64, 113 height: 64, 114 }, 115 ]}> 116 <ListIcon size="2xl" fill={t.atoms.text_contrast_medium.color} /> 117 </View> 118 <Text 119 style={[ 120 a.text_center, 121 a.flex_1, 122 a.text_sm, 123 a.leading_snug, 124 t.atoms.text_contrast_medium, 125 { 126 maxWidth: 200, 127 }, 128 ]}> 129 {emptyText} 130 </Text> 131 </View> 132 ) 133 } else if (item === ERROR_ITEM) { 134 return ( 135 <ErrorMessage 136 message={cleanError(error)} 137 onPressTryAgain={onRefresh} 138 /> 139 ) 140 } else if (item === LOADING) { 141 return ( 142 <View style={{padding: 20}}> 143 <ActivityIndicator /> 144 </View> 145 ) 146 } 147 return renderItem ? ( 148 renderItem(item, index) 149 ) : ( 150 <View 151 style={[ 152 index !== 0 && a.border_t, 153 t.atoms.border_contrast_low, 154 a.px_lg, 155 a.py_lg, 156 ]}> 157 <ListCard.Default view={item} /> 158 </View> 159 ) 160 }, 161 [t, renderItem, error, onRefresh, emptyText], 162 ) 163 164 if (inline) { 165 return ( 166 <View testID={testID} style={style}> 167 {items.length > 0 && ( 168 <RNFlatList 169 testID={testID ? `${testID}-flatlist` : undefined} 170 data={items} 171 keyExtractor={item => (item.uri ? item.uri : item._reactKey)} 172 renderItem={renderItemInner} 173 refreshControl={ 174 <RefreshControl 175 refreshing={isPTRing} 176 onRefresh={onRefresh} 177 tintColor={pal.colors.text} 178 titleColor={pal.colors.text} 179 /> 180 } 181 contentContainerStyle={[s.contentContainer]} 182 removeClippedSubviews={true} 183 /> 184 )} 185 </View> 186 ) 187 } else { 188 return ( 189 <View testID={testID} style={style}> 190 {items.length > 0 && ( 191 <List 192 testID={testID ? `${testID}-flatlist` : undefined} 193 data={items} 194 keyExtractor={item => (item.uri ? item.uri : item._reactKey)} 195 renderItem={renderItemInner} 196 refreshing={isPTRing} 197 onRefresh={onRefresh} 198 contentContainerStyle={[s.contentContainer]} 199 removeClippedSubviews={true} 200 desktopFixedHeight 201 sideBorders={false} 202 /> 203 )} 204 </View> 205 ) 206 } 207}