Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 210 lines 6.2 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 {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 = useMemo(() => { 46 return enforceLen(decodeURIComponent(topic), 24, true, 'middle') 47 }, [topic]) 48 49 const onShare = 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] = useState(0) 56 const setMinimalShellMode = useSetMinimalShellMode() 57 58 useFocusEffect( 59 useCallback(() => { 60 setMinimalShellMode(false) 61 }, [setMinimalShellMode]), 62 ) 63 64 const onPageSelected = useCallback( 65 (index: number) => { 66 setMinimalShellMode(false) 67 setActiveTab(index) 68 }, 69 [setMinimalShellMode], 70 ) 71 72 const sections = 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] = 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 = useMemo(() => { 161 return data?.pages.flatMap(page => page.posts) || [] 162 }, [data]) 163 164 const onRefresh = useCallback(async () => { 165 setIsPTR(true) 166 await refetch() 167 setIsPTR(false) 168 }, [refetch]) 169 170 const onEndReached = 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}