this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at e28f6d2f370b4e882ed6f23d08ca0f8d94dbac5f 207 lines 6.0 kB view raw
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}