Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

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