Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import {useCallback, useState} from 'react'
2import {
3 type AppBskyFeedDefs,
4 AppBskyFeedPost,
5 moderatePost,
6 type ModerationDecision,
7} from '@atproto/api'
8import {msg} from '@lingui/core/macro'
9import {useLingui} from '@lingui/react'
10
11import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
12import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
13import {cleanError} from '#/lib/strings/errors'
14import {logger} from '#/logger'
15import {useModerationOpts} from '#/state/preferences/moderation-opts'
16import {usePostQuotesQuery} from '#/state/queries/post-quotes'
17import {useResolveUriQuery} from '#/state/queries/resolve-uri'
18import {Post} from '#/view/com/post/Post'
19import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
20import {List} from '../util/List'
21
22function renderItem({
23 item,
24 index,
25}: {
26 item: {
27 post: AppBskyFeedDefs.PostView
28 moderation: ModerationDecision
29 record: AppBskyFeedPost.Record
30 }
31 index: number
32}) {
33 return <Post post={item.post} hideTopBorder={index === 0} />
34}
35
36function keyExtractor(item: {
37 post: AppBskyFeedDefs.PostView
38 moderation: ModerationDecision
39 record: AppBskyFeedPost.Record
40}) {
41 return item.post.uri
42}
43
44export function PostQuotes({uri}: {uri: string}) {
45 const {_} = useLingui()
46 const initialNumToRender = useInitialNumToRender()
47 const [isPTRing, setIsPTRing] = useState(false)
48 const trackPostView = usePostViewTracking('PostQuotes')
49
50 const {
51 data: resolvedUri,
52 error: resolveError,
53 isLoading: isLoadingUri,
54 } = useResolveUriQuery(uri)
55 const {
56 data,
57 isLoading: isLoadingQuotes,
58 isFetchingNextPage,
59 hasNextPage,
60 fetchNextPage,
61 error,
62 refetch,
63 } = usePostQuotesQuery(resolvedUri?.uri)
64
65 const moderationOpts = useModerationOpts()
66
67 const isError = Boolean(resolveError || error)
68
69 const quotes =
70 data?.pages
71 .flatMap(page =>
72 page.posts.map(post => {
73 if (!AppBskyFeedPost.isRecord(post.record) || !moderationOpts) {
74 return null
75 }
76 const moderation = moderatePost(post, moderationOpts)
77 return {post, record: post.record, moderation}
78 }),
79 )
80 .filter(item => item !== null) ?? []
81
82 const onRefresh = useCallback(async () => {
83 setIsPTRing(true)
84 try {
85 await refetch()
86 } catch (err) {
87 logger.error('Failed to refresh quotes', {message: err})
88 }
89 setIsPTRing(false)
90 }, [refetch, setIsPTRing])
91
92 const onEndReached = useCallback(async () => {
93 if (isFetchingNextPage || !hasNextPage || isError) return
94 try {
95 await fetchNextPage()
96 } catch (err) {
97 logger.error('Failed to load more quotes', {message: err})
98 }
99 }, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
100
101 if (quotes.length < 1) {
102 return (
103 <ListMaybePlaceholder
104 isLoading={isLoadingUri || isLoadingQuotes}
105 isError={isError}
106 emptyType="results"
107 emptyTitle={_(msg`No quotes yet`)}
108 emptyMessage={_(
109 msg`Nobody has quoted this yet. Maybe you should be the first!`,
110 )}
111 errorMessage={cleanError(resolveError || error)}
112 sideBorders={false}
113 />
114 )
115 }
116
117 // loaded
118 // =
119 return (
120 <List
121 data={quotes}
122 renderItem={renderItem}
123 keyExtractor={keyExtractor}
124 refreshing={isPTRing}
125 onRefresh={onRefresh}
126 onEndReached={onEndReached}
127 onEndReachedThreshold={4}
128 onItemSeen={item => trackPostView(item.post)}
129 ListFooterComponent={
130 <ListFooter
131 isFetchingNextPage={isFetchingNextPage}
132 error={cleanError(error)}
133 onRetry={fetchNextPage}
134 showEndMessage
135 endMessageText={_(msg`That's all, folks!`)}
136 />
137 }
138 // @ts-ignore our .web version only -prf
139 desktopFixedHeight
140 initialNumToRender={initialNumToRender}
141 windowSize={11}
142 sideBorders={false}
143 />
144 )
145}