forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}