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