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