mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at tooltip 204 lines 5.9 kB view raw
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}