Bluesky app fork with some witchin' additions 馃挮
at main 6.2 kB view raw
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}