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