forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useMemo, useState} from 'react'
2import {type ListRenderItemInfo, View} from 'react-native'
3import {type AppBskyFeedDefs} from '@atproto/api'
4import {msg} from '@lingui/core/macro'
5import {useLingui} from '@lingui/react'
6import {type NativeStackScreenProps} from '@react-navigation/native-stack'
7
8import {HITSLOP_10} from '#/lib/constants'
9import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
10import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking'
11import {type CommonNavigatorParams} from '#/lib/routes/types'
12import {shareUrl} from '#/lib/sharing'
13import {cleanError} from '#/lib/strings/errors'
14import {enforceLen} from '#/lib/strings/helpers'
15import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
16import {useSearchPostsQuery} from '#/state/queries/search-posts'
17import {Pager} from '#/view/com/pager/Pager'
18import {TabBar} from '#/view/com/pager/TabBar'
19import {Post} from '#/view/com/post/Post'
20import {List} from '#/view/com/util/List'
21import {atoms as a, web} from '#/alf'
22import {Button, ButtonIcon} from '#/components/Button'
23import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
24import * as Layout from '#/components/Layout'
25import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
26
27const renderItem = ({item}: ListRenderItemInfo<AppBskyFeedDefs.PostView>) => {
28 return <Post post={item} />
29}
30
31const keyExtractor = (item: AppBskyFeedDefs.PostView, index: number) => {
32 return `${item.uri}-${index}`
33}
34
35export default function TopicScreen({
36 route,
37}: NativeStackScreenProps<CommonNavigatorParams, 'Topic'>) {
38 const {topic} = route.params
39 const {_} = useLingui()
40
41 const enableSquareButtons = useEnableSquareButtons()
42
43 const headerTitle = useMemo(() => {
44 return enforceLen(decodeURIComponent(topic), 24, true, 'middle')
45 }, [topic])
46
47 const onShare = useCallback(() => {
48 const url = new URL('https://witchsky.app')
49 url.pathname = `/topic/${topic}`
50 shareUrl(url.toString())
51 }, [topic])
52
53 const [activeTab, setActiveTab] = useState(0)
54
55 const onPageSelected = (index: number) => {
56 setActiveTab(index)
57 }
58
59 const sections = useMemo(() => {
60 return [
61 {
62 title: _(msg`Top`),
63 component: (
64 <TopicScreenTab topic={topic} sort="top" active={activeTab === 0} />
65 ),
66 },
67 {
68 title: _(msg`Latest`),
69 component: (
70 <TopicScreenTab
71 topic={topic}
72 sort="latest"
73 active={activeTab === 1}
74 />
75 ),
76 },
77 ]
78 }, [_, topic, activeTab])
79
80 return (
81 <Layout.Screen>
82 <Pager
83 onPageSelected={onPageSelected}
84 renderTabBar={props => (
85 <Layout.Center style={[a.z_10, web([a.sticky, {top: 0}])]}>
86 <Layout.Header.Outer noBottomBorder>
87 <Layout.Header.BackButton />
88 <Layout.Header.Content>
89 <Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText>
90 </Layout.Header.Content>
91 <Layout.Header.Slot>
92 <Button
93 label={_(msg`Share`)}
94 size="small"
95 variant="ghost"
96 color="primary"
97 shape={enableSquareButtons ? 'square' : 'round'}
98 onPress={onShare}
99 hitSlop={HITSLOP_10}
100 style={[{right: -3}]}>
101 <ButtonIcon icon={Share} size="md" />
102 </Button>
103 </Layout.Header.Slot>
104 </Layout.Header.Outer>
105 <TabBar items={sections.map(section => section.title)} {...props} />
106 </Layout.Center>
107 )}
108 initialPage={0}>
109 {sections.map((section, i) => (
110 <View key={i}>{section.component}</View>
111 ))}
112 </Pager>
113 </Layout.Screen>
114 )
115}
116
117function TopicScreenTab({
118 topic,
119 sort,
120 active,
121}: {
122 topic: string
123 sort: 'top' | 'latest'
124 active: boolean
125}) {
126 const {_} = useLingui()
127 const initialNumToRender = useInitialNumToRender()
128 const [isPTR, setIsPTR] = useState(false)
129 const trackPostView = usePostViewTracking('Topic')
130
131 const {
132 data,
133 isFetched,
134 isFetchingNextPage,
135 isLoading,
136 isError,
137 error,
138 refetch,
139 fetchNextPage,
140 hasNextPage,
141 } = useSearchPostsQuery({
142 query: decodeURIComponent(topic),
143 sort,
144 enabled: active,
145 })
146
147 const posts = useMemo(() => {
148 return data?.pages.flatMap(page => page.posts) || []
149 }, [data])
150
151 const onRefresh = useCallback(async () => {
152 setIsPTR(true)
153 await refetch()
154 setIsPTR(false)
155 }, [refetch])
156
157 const onEndReached = useCallback(() => {
158 if (isFetchingNextPage || !hasNextPage || error) return
159 fetchNextPage()
160 }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
161
162 return (
163 <>
164 {posts.length < 1 ? (
165 <ListMaybePlaceholder
166 isLoading={isLoading || !isFetched}
167 isError={isError}
168 onRetry={refetch}
169 emptyType="results"
170 emptyMessage={_(msg`We couldn't find any results for that topic.`)}
171 />
172 ) : (
173 <List
174 data={posts}
175 renderItem={renderItem}
176 keyExtractor={keyExtractor}
177 refreshing={isPTR}
178 onRefresh={onRefresh}
179 onEndReached={onEndReached}
180 onEndReachedThreshold={4}
181 onItemSeen={trackPostView}
182 // @ts-ignore web only -prf
183 desktopFixedHeight
184 ListFooterComponent={
185 <ListFooter
186 isFetchingNextPage={isFetchingNextPage}
187 error={cleanError(error)}
188 onRetry={fetchNextPage}
189 />
190 }
191 initialNumToRender={initialNumToRender}
192 windowSize={11}
193 />
194 )}
195 </>
196 )
197}