mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react'
2import {ListRenderItemInfo, Pressable, 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 {isNative, isWeb} from 'platform/detection'
17import {useSearchPostsQuery} from 'state/queries/search-posts'
18import {useSetDrawerSwipeDisabled, useSetMinimalShellMode} from 'state/shell'
19import {Pager} from '#/view/com/pager/Pager'
20import {TabBar} from '#/view/com/pager/TabBar'
21import {CenteredView} from '#/view/com/util/Views'
22import {Post} from 'view/com/post/Post'
23import {List} from 'view/com/util/List'
24import {ViewHeader} from 'view/com/util/ViewHeader'
25import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
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 const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
67
68 useFocusEffect(
69 React.useCallback(() => {
70 setMinimalShellMode(false)
71 }, [setMinimalShellMode]),
72 )
73
74 const onPageSelected = React.useCallback(
75 (index: number) => {
76 setMinimalShellMode(false)
77 setDrawerSwipeDisabled(index > 0)
78 setActiveTab(index)
79 },
80 [setDrawerSwipeDisabled, setMinimalShellMode],
81 )
82
83 const sections = React.useMemo(() => {
84 return [
85 {
86 title: _(msg`Top`),
87 component: (
88 <HashtagScreenTab
89 fullTag={fullTag}
90 author={author}
91 sort="top"
92 active={activeTab === 0}
93 />
94 ),
95 },
96 {
97 title: _(msg`Latest`),
98 component: (
99 <HashtagScreenTab
100 fullTag={fullTag}
101 author={author}
102 sort="latest"
103 active={activeTab === 1}
104 />
105 ),
106 },
107 ]
108 }, [_, fullTag, author, activeTab])
109
110 return (
111 <>
112 <CenteredView sideBorders={true}>
113 <ViewHeader
114 showOnDesktop
115 title={headerTitle}
116 subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
117 canGoBack
118 renderButton={
119 isNative
120 ? () => (
121 <Pressable
122 accessibilityRole="button"
123 onPress={onShare}
124 hitSlop={HITSLOP_10}>
125 <ArrowOutOfBox_Stroke2_Corner0_Rounded
126 size="lg"
127 onPress={onShare}
128 />
129 </Pressable>
130 )
131 : undefined
132 }
133 />
134 </CenteredView>
135 <Pager
136 onPageSelected={onPageSelected}
137 renderTabBar={props => (
138 <CenteredView
139 sideBorders={true}
140 // @ts-ignore web only
141 style={
142 isWeb
143 ? {
144 position: isWeb ? 'sticky' : '',
145 top: 0,
146 zIndex: 1,
147 }
148 : undefined
149 }>
150 <TabBar items={sections.map(section => section.title)} {...props} />
151 </CenteredView>
152 )}
153 initialPage={0}>
154 {sections.map((section, i) => (
155 <View key={i}>{section.component}</View>
156 ))}
157 </Pager>
158 </>
159 )
160}
161
162function HashtagScreenTab({
163 fullTag,
164 author,
165 sort,
166 active,
167}: {
168 fullTag: string
169 author: string | undefined
170 sort: 'top' | 'latest'
171 active: boolean
172}) {
173 const {_} = useLingui()
174 const initialNumToRender = useInitialNumToRender()
175 const [isPTR, setIsPTR] = React.useState(false)
176
177 const queryParam = React.useMemo(() => {
178 if (!author) return fullTag
179 return `${fullTag} from:${author}`
180 }, [fullTag, author])
181
182 const {
183 data,
184 isFetched,
185 isFetchingNextPage,
186 isLoading,
187 isError,
188 error,
189 refetch,
190 fetchNextPage,
191 hasNextPage,
192 } = useSearchPostsQuery({query: queryParam, sort, enabled: active})
193
194 const posts = React.useMemo(() => {
195 return data?.pages.flatMap(page => page.posts) || []
196 }, [data])
197
198 const onRefresh = React.useCallback(async () => {
199 setIsPTR(true)
200 await refetch()
201 setIsPTR(false)
202 }, [refetch])
203
204 const onEndReached = React.useCallback(() => {
205 if (isFetchingNextPage || !hasNextPage || error) return
206 fetchNextPage()
207 }, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
208
209 return (
210 <>
211 {posts.length < 1 ? (
212 <ListMaybePlaceholder
213 isLoading={isLoading || !isFetched}
214 isError={isError}
215 onRetry={refetch}
216 emptyType="results"
217 emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
218 />
219 ) : (
220 <List
221 data={posts}
222 renderItem={renderItem}
223 keyExtractor={keyExtractor}
224 refreshing={isPTR}
225 onRefresh={onRefresh}
226 onEndReached={onEndReached}
227 onEndReachedThreshold={4}
228 // @ts-ignore web only -prf
229 desktopFixedHeight
230 ListFooterComponent={
231 <ListFooter
232 isFetchingNextPage={isFetchingNextPage}
233 error={cleanError(error)}
234 onRetry={fetchNextPage}
235 />
236 }
237 initialNumToRender={initialNumToRender}
238 windowSize={11}
239 />
240 )}
241 </>
242 )
243}