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