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 {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}