Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import {memo} from 'react'
2import {type StyleProp, View, type ViewStyle} from 'react-native'
3import {msg} from '@lingui/core/macro'
4import {useLingui} from '@lingui/react'
5import {Trans} from '@lingui/react/macro'
6
7import {cleanError} from '#/lib/strings/errors'
8import {
9 EmptyState,
10 type EmptyStateButtonProps,
11} from '#/view/com/util/EmptyState'
12import {CenteredView} from '#/view/com/util/Views'
13import {atoms as a, useBreakpoints, useTheme} from '#/alf'
14import {Button, ButtonText} from '#/components/Button'
15import {Error} from '#/components/Error'
16import {Loader} from '#/components/Loader'
17import {Text} from '#/components/Typography'
18
19export function ListFooter({
20 isFetchingNextPage,
21 hasNextPage,
22 error,
23 onRetry,
24 height,
25 style,
26 showEndMessage = false,
27 endMessageText,
28 renderEndMessage,
29}: {
30 isFetchingNextPage?: boolean
31 hasNextPage?: boolean
32 error?: string
33 onRetry?: () => Promise<unknown>
34 height?: number
35 style?: StyleProp<ViewStyle>
36 showEndMessage?: boolean
37 endMessageText?: string
38 renderEndMessage?: () => React.ReactNode
39}) {
40 const t = useTheme()
41
42 return (
43 <View
44 style={[
45 a.w_full,
46 a.align_center,
47 a.border_t,
48 a.pb_lg,
49 t.atoms.border_contrast_low,
50 {height: height ?? 180, paddingTop: 30},
51 style,
52 ]}>
53 {isFetchingNextPage ? (
54 <Loader size="xl" />
55 ) : error ? (
56 <ListFooterMaybeError error={error} onRetry={onRetry} />
57 ) : !hasNextPage && showEndMessage ? (
58 renderEndMessage ? (
59 renderEndMessage()
60 ) : (
61 <Text style={[a.text_sm, t.atoms.text_contrast_low]}>
62 {endMessageText ?? <Trans>You have reached the end</Trans>}
63 </Text>
64 )
65 ) : null}
66 </View>
67 )
68}
69
70function ListFooterMaybeError({
71 error,
72 onRetry,
73}: {
74 error?: string
75 onRetry?: () => Promise<unknown>
76}) {
77 const t = useTheme()
78 const {_} = useLingui()
79
80 if (!error) return null
81
82 return (
83 <View style={[a.w_full, a.px_lg]}>
84 <View
85 style={[
86 a.flex_row,
87 a.gap_md,
88 a.p_md,
89 a.rounded_sm,
90 a.align_center,
91 t.atoms.bg_contrast_25,
92 ]}>
93 <Text
94 style={[a.flex_1, a.text_sm, t.atoms.text_contrast_medium]}
95 numberOfLines={2}>
96 {error ? (
97 cleanError(error)
98 ) : (
99 <Trans>Oops, something went wrong!</Trans>
100 )}
101 </Text>
102 <Button
103 variant="solid"
104 label={_(msg`Press to retry`)}
105 style={[
106 a.align_center,
107 a.justify_center,
108 a.rounded_sm,
109 a.overflow_hidden,
110 a.px_md,
111 a.py_sm,
112 ]}
113 onPress={onRetry}>
114 <ButtonText>
115 <Trans>Retry</Trans>
116 </ButtonText>
117 </Button>
118 </View>
119 </View>
120 )
121}
122
123let ListMaybePlaceholder = ({
124 isLoading,
125 noEmpty,
126 isError,
127 emptyTitle,
128 emptyMessage,
129 errorTitle,
130 errorMessage,
131 emptyType = 'page',
132 onRetry,
133 onGoBack,
134 hideBackButton,
135 sideBorders,
136 topBorder = false,
137 emptyStateIcon,
138 emptyStateButton,
139 useEmptyState = false,
140}: {
141 isLoading: boolean
142 noEmpty?: boolean
143 isError?: boolean
144 emptyTitle?: string
145 emptyMessage?: string
146 errorTitle?: string
147 errorMessage?: string
148 emptyType?: 'page' | 'results'
149 onRetry?: () => Promise<unknown>
150 onGoBack?: () => void
151 hideBackButton?: boolean
152 sideBorders?: boolean
153 topBorder?: boolean
154 emptyStateIcon?: React.ComponentType<any> | React.ReactElement
155 emptyStateButton?: EmptyStateButtonProps
156 useEmptyState?: boolean
157}): React.ReactNode => {
158 const t = useTheme()
159 const {_} = useLingui()
160 const {gtMobile, gtTablet} = useBreakpoints()
161
162 if (isLoading) {
163 return (
164 <CenteredView
165 style={[
166 a.h_full_vh,
167 a.align_center,
168 !gtMobile ? a.justify_between : a.gap_5xl,
169 t.atoms.border_contrast_low,
170 {paddingTop: 175, paddingBottom: 110},
171 ]}
172 sideBorders={sideBorders ?? gtMobile}
173 topBorder={topBorder && !gtTablet}>
174 <View style={[a.w_full, a.align_center, {top: 100}]}>
175 <Loader size="xl" />
176 </View>
177 </CenteredView>
178 )
179 }
180
181 if (isError) {
182 return (
183 <Error
184 title={errorTitle ?? _(msg`Oops!`)}
185 message={errorMessage ?? _(msg`Something went wrong!`)}
186 onRetry={onRetry}
187 onGoBack={onGoBack}
188 sideBorders={sideBorders}
189 hideBackButton={hideBackButton}
190 />
191 )
192 }
193
194 if (useEmptyState) {
195 return (
196 <CenteredView
197 style={[t.atoms.border_contrast_low]}
198 sideBorders={sideBorders ?? gtMobile}>
199 <EmptyState
200 icon={emptyStateIcon}
201 message={
202 emptyMessage ??
203 (emptyType === 'results'
204 ? _(msg`No results found`)
205 : _(msg`Page not found`))
206 }
207 button={emptyStateButton}
208 />
209 </CenteredView>
210 )
211 }
212
213 if (!noEmpty) {
214 return (
215 <Error
216 title={
217 emptyTitle ??
218 (emptyType === 'results'
219 ? _(msg`No results found`)
220 : _(msg`Page not found`))
221 }
222 message={
223 emptyMessage ??
224 _(msg`We're sorry! We can't find the page you were looking for.`)
225 }
226 onRetry={onRetry}
227 onGoBack={onGoBack}
228 hideBackButton={hideBackButton}
229 sideBorders={sideBorders}
230 />
231 )
232 }
233
234 return null
235}
236ListMaybePlaceholder = memo(ListMaybePlaceholder)
237export {ListMaybePlaceholder}