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