forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useMemo} from 'react'
2import {
3 type DimensionValue,
4 type StyleProp,
5 StyleSheet,
6 View,
7 type ViewStyle,
8} from 'react-native'
9
10import {s} from '#/lib/styles'
11import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars'
12import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
13import {atoms as a, useTheme} from '#/alf'
14import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '#/components/icons/Bubble'
15import {
16 Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled,
17 Heart2_Stroke2_Corner0_Rounded as HeartIconOutline,
18} from '#/components/icons/Heart2'
19import {Repost_Stroke2_Corner2_Rounded as Repost} from '#/components/icons/Repost'
20
21export function LoadingPlaceholder({
22 width,
23 height,
24 style,
25}: {
26 width: DimensionValue
27 height: DimensionValue | undefined
28 style?: StyleProp<ViewStyle>
29}) {
30 const t = useTheme()
31 return (
32 <View
33 style={[
34 styles.loadingPlaceholder,
35 {
36 width,
37 height,
38 backgroundColor: t.palette.contrast_50,
39 },
40 style,
41 ]}
42 />
43 )
44}
45
46export function PostLoadingPlaceholder({
47 style,
48}: {
49 style?: StyleProp<ViewStyle>
50}) {
51 const t = useTheme()
52 const enableSquareAvatars = useEnableSquareAvatars()
53 return (
54 <View style={[styles.post, style]}>
55 <LoadingPlaceholder
56 width={42}
57 height={42}
58 style={[
59 styles.avatar,
60 {
61 position: 'relative',
62 top: -6,
63 },
64 enableSquareAvatars && {borderRadius: 8},
65 ]}
66 />
67 <View style={[a.flex_1]}>
68 <LoadingPlaceholder width={100} height={6} style={{marginBottom: 10}} />
69 <LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} />
70 <LoadingPlaceholder width="95%" height={6} style={{marginBottom: 8}} />
71 <LoadingPlaceholder width="80%" height={6} style={{marginBottom: 11}} />
72 <View style={styles.postCtrls}>
73 <View style={[styles.postCtrl, {marginLeft: -6}]}>
74 <View style={styles.postBtn}>
75 <Bubble
76 style={[
77 {
78 color: t.palette.contrast_500,
79 },
80 {pointerEvents: 'none'},
81 ]}
82 width={18}
83 />
84 </View>
85 </View>
86 <View style={styles.postCtrl}>
87 <View style={styles.postBtn}>
88 <Repost
89 style={[
90 {
91 color: t.palette.contrast_500,
92 },
93 {pointerEvents: 'none'},
94 ]}
95 width={18}
96 />
97 </View>
98 </View>
99 <View style={styles.postCtrl}>
100 <View style={styles.postBtn}>
101 <HeartIconOutline
102 style={[
103 {
104 color: t.palette.contrast_500,
105 },
106 {pointerEvents: 'none'},
107 ]}
108 width={18}
109 />
110 </View>
111 </View>
112 <View style={styles.postCtrl}>
113 <View style={[styles.postBtn, {minHeight: 30}]} />
114 </View>
115 </View>
116 </View>
117 </View>
118 )
119}
120
121export function PostFeedLoadingPlaceholder() {
122 return (
123 <View>
124 <PostLoadingPlaceholder />
125 <PostLoadingPlaceholder />
126 <PostLoadingPlaceholder />
127 <PostLoadingPlaceholder />
128 <PostLoadingPlaceholder />
129 <PostLoadingPlaceholder />
130 <PostLoadingPlaceholder />
131 <PostLoadingPlaceholder />
132 </View>
133 )
134}
135
136export function NotificationLoadingPlaceholder({
137 style,
138}: {
139 style?: StyleProp<ViewStyle>
140}) {
141 const t = useTheme()
142 const enableSquareAvatars = useEnableSquareAvatars()
143 return (
144 <View style={[styles.notification, style]}>
145 <View style={[{width: 60}, a.align_end, a.pr_sm, a.pt_2xs]}>
146 <HeartIconFilled size="xl" style={{color: t.palette.contrast_50}} />
147 </View>
148 <View style={{flex: 1}}>
149 <View style={[a.flex_row, s.mb10]}>
150 <LoadingPlaceholder
151 width={35}
152 height={35}
153 style={[
154 styles.smallAvatar,
155 enableSquareAvatars && {borderRadius: 8},
156 ]}
157 />
158 </View>
159 <LoadingPlaceholder width="90%" height={6} style={[s.mb5]} />
160 <LoadingPlaceholder width="70%" height={6} style={[s.mb5]} />
161 </View>
162 </View>
163 )
164}
165
166export function NotificationFeedLoadingPlaceholder() {
167 return (
168 <>
169 <NotificationLoadingPlaceholder />
170 <NotificationLoadingPlaceholder />
171 <NotificationLoadingPlaceholder />
172 <NotificationLoadingPlaceholder />
173 <NotificationLoadingPlaceholder />
174 <NotificationLoadingPlaceholder />
175 <NotificationLoadingPlaceholder />
176 <NotificationLoadingPlaceholder />
177 <NotificationLoadingPlaceholder />
178 <NotificationLoadingPlaceholder />
179 <NotificationLoadingPlaceholder />
180 </>
181 )
182}
183
184export function ProfileCardLoadingPlaceholder({
185 style,
186}: {
187 style?: StyleProp<ViewStyle>
188}) {
189 return (
190 <View style={[styles.profileCard, style]}>
191 <LoadingPlaceholder
192 width={40}
193 height={40}
194 style={styles.profileCardAvi}
195 />
196 <View>
197 <LoadingPlaceholder width={140} height={8} style={[s.mb5]} />
198 <LoadingPlaceholder width={120} height={8} style={[s.mb10]} />
199 <LoadingPlaceholder width={220} height={8} style={[s.mb5]} />
200 </View>
201 </View>
202 )
203}
204
205export function ProfileCardFeedLoadingPlaceholder() {
206 return (
207 <>
208 <ProfileCardLoadingPlaceholder />
209 <ProfileCardLoadingPlaceholder />
210 <ProfileCardLoadingPlaceholder />
211 <ProfileCardLoadingPlaceholder />
212 <ProfileCardLoadingPlaceholder />
213 <ProfileCardLoadingPlaceholder />
214 <ProfileCardLoadingPlaceholder />
215 <ProfileCardLoadingPlaceholder />
216 <ProfileCardLoadingPlaceholder />
217 <ProfileCardLoadingPlaceholder />
218 <ProfileCardLoadingPlaceholder />
219 </>
220 )
221}
222
223export function FeedLoadingPlaceholder({
224 style,
225 showLowerPlaceholder = true,
226 showTopBorder = true,
227}: {
228 style?: StyleProp<ViewStyle>
229 showTopBorder?: boolean
230 showLowerPlaceholder?: boolean
231}) {
232 const t = useTheme()
233 return (
234 <View
235 style={[
236 {
237 padding: 16,
238 borderTopWidth: showTopBorder ? StyleSheet.hairlineWidth : 0,
239 },
240 t.atoms.border_contrast_low,
241 style,
242 ]}>
243 <View style={[{flexDirection: 'row'}]}>
244 <LoadingPlaceholder
245 width={36}
246 height={36}
247 style={[styles.avatar, {borderRadius: 8}]}
248 />
249 <View style={[a.flex_1]}>
250 <LoadingPlaceholder width={100} height={8} style={[s.mt5, s.mb10]} />
251 <LoadingPlaceholder width={120} height={8} />
252 </View>
253 </View>
254 {showLowerPlaceholder && (
255 <View style={{marginTop: 12}}>
256 <LoadingPlaceholder width={120} height={8} />
257 </View>
258 )}
259 </View>
260 )
261}
262
263export function FeedFeedLoadingPlaceholder() {
264 return (
265 <>
266 <FeedLoadingPlaceholder />
267 <FeedLoadingPlaceholder />
268 <FeedLoadingPlaceholder />
269 <FeedLoadingPlaceholder />
270 <FeedLoadingPlaceholder />
271 <FeedLoadingPlaceholder />
272 <FeedLoadingPlaceholder />
273 <FeedLoadingPlaceholder />
274 <FeedLoadingPlaceholder />
275 <FeedLoadingPlaceholder />
276 <FeedLoadingPlaceholder />
277 </>
278 )
279}
280
281export function ChatListItemLoadingPlaceholder({
282 style,
283}: {
284 style?: StyleProp<ViewStyle>
285}) {
286 const t = useTheme()
287 const random = useMemo(() => Math.random(), [])
288 const enableSquareButtons = useEnableSquareButtons()
289 return (
290 <View style={[a.flex_row, a.gap_md, a.px_lg, a.mt_lg, t.atoms.bg, style]}>
291 <LoadingPlaceholder
292 width={52}
293 height={52}
294 style={enableSquareButtons ? a.rounded_sm : a.rounded_full}
295 />
296 <View>
297 <LoadingPlaceholder width={140} height={12} style={a.mt_xs} />
298 <LoadingPlaceholder width={120} height={8} style={a.mt_sm} />
299 <LoadingPlaceholder
300 width={80 + random * 100}
301 height={8}
302 style={a.mt_sm}
303 />
304 </View>
305 </View>
306 )
307}
308
309export function ChatListLoadingPlaceholder() {
310 return (
311 <>
312 <ChatListItemLoadingPlaceholder />
313 <ChatListItemLoadingPlaceholder />
314 <ChatListItemLoadingPlaceholder />
315 <ChatListItemLoadingPlaceholder />
316 <ChatListItemLoadingPlaceholder />
317 <ChatListItemLoadingPlaceholder />
318 <ChatListItemLoadingPlaceholder />
319 <ChatListItemLoadingPlaceholder />
320 <ChatListItemLoadingPlaceholder />
321 <ChatListItemLoadingPlaceholder />
322 <ChatListItemLoadingPlaceholder />
323 </>
324 )
325}
326
327const styles = StyleSheet.create({
328 loadingPlaceholder: {
329 borderRadius: 6,
330 },
331 post: {
332 flexDirection: 'row',
333 alignItems: 'flex-start',
334 paddingHorizontal: 10,
335 paddingTop: 20,
336 paddingBottom: 5,
337 paddingRight: 15,
338 },
339 postCtrls: {
340 opacity: 0.5,
341 flexDirection: 'row',
342 justifyContent: 'space-between',
343 },
344 postCtrl: {
345 flex: 1,
346 },
347 postBtn: {
348 flex: 1,
349 flexDirection: 'row',
350 alignItems: 'center',
351 padding: 5,
352 },
353 avatar: {
354 borderRadius: 999,
355 marginRight: 12,
356 },
357 notification: {
358 flexDirection: 'row',
359 padding: 10,
360 },
361 profileCard: {
362 flexDirection: 'row',
363 padding: 10,
364 margin: 1,
365 },
366 profileCardAvi: {
367 borderRadius: 999,
368 marginRight: 10,
369 },
370 smallAvatar: {
371 borderRadius: 999,
372 marginRight: 10,
373 },
374})