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

Configure Feed

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

at a1857d62bd5fef23b7e8a8a184ed19d02a95f604 285 lines 8.6 kB view raw
1import React from 'react' 2import {type ListRenderItemInfo, View} from 'react-native' 3import {type AppBskyFeedDefs} from '@atproto/api' 4import {msg, Trans} 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 {sanitizeHandle} from '#/lib/strings/handles' 16import {enforceLen} from '#/lib/strings/helpers' 17import {useSearchPostsQuery} from '#/state/queries/search-posts' 18import {useSession} from '#/state/session' 19import {useSetMinimalShellMode} from '#/state/shell' 20import {useLoggedOutViewControls} from '#/state/shell/logged-out' 21import {useCloseAllActiveElements} from '#/state/util' 22import {Pager} from '#/view/com/pager/Pager' 23import {TabBar} from '#/view/com/pager/TabBar' 24import {Post} from '#/view/com/post/Post' 25import {List} from '#/view/com/util/List' 26import {atoms as a, useTheme, web} from '#/alf' 27import {Button, ButtonIcon} from '#/components/Button' 28import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox' 29import * as Layout from '#/components/Layout' 30import {InlineLinkText} from '#/components/Link' 31import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' 32import {SearchError} from '#/components/SearchError' 33import {Text} from '#/components/Typography' 34 35const renderItem = ({item}: ListRenderItemInfo<AppBskyFeedDefs.PostView>) => { 36 return <Post post={item} /> 37} 38 39const keyExtractor = (item: AppBskyFeedDefs.PostView, index: number) => { 40 return `${item.uri}-${index}` 41} 42 43export default function HashtagScreen({ 44 route, 45}: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) { 46 const {tag, author} = route.params 47 const {_} = useLingui() 48 49 const fullTag = React.useMemo(() => { 50 return `#${decodeURIComponent(tag)}` 51 }, [tag]) 52 53 const headerTitle = React.useMemo(() => { 54 return enforceLen(fullTag.toLowerCase(), 24, true, 'middle') 55 }, [fullTag]) 56 57 const sanitizedAuthor = React.useMemo(() => { 58 if (!author) return 59 return sanitizeHandle(author) 60 }, [author]) 61 62 const onShare = React.useCallback(() => { 63 const url = new URL('https://bsky.app') 64 url.pathname = `/hashtag/${decodeURIComponent(tag)}` 65 if (author) { 66 url.searchParams.set('author', author) 67 } 68 shareUrl(url.toString()) 69 }, [tag, author]) 70 71 const [activeTab, setActiveTab] = React.useState(0) 72 const setMinimalShellMode = useSetMinimalShellMode() 73 74 useFocusEffect( 75 React.useCallback(() => { 76 setMinimalShellMode(false) 77 }, [setMinimalShellMode]), 78 ) 79 80 const onPageSelected = React.useCallback( 81 (index: number) => { 82 setMinimalShellMode(false) 83 setActiveTab(index) 84 }, 85 [setMinimalShellMode], 86 ) 87 88 const sections = React.useMemo(() => { 89 return [ 90 { 91 title: _(msg`Top`), 92 component: ( 93 <HashtagScreenTab 94 fullTag={fullTag} 95 author={author} 96 sort="top" 97 active={activeTab === 0} 98 /> 99 ), 100 }, 101 { 102 title: _(msg`Latest`), 103 component: ( 104 <HashtagScreenTab 105 fullTag={fullTag} 106 author={author} 107 sort="latest" 108 active={activeTab === 1} 109 /> 110 ), 111 }, 112 ] 113 }, [_, fullTag, author, activeTab]) 114 115 return ( 116 <Layout.Screen> 117 <Pager 118 onPageSelected={onPageSelected} 119 renderTabBar={props => ( 120 <Layout.Center style={[a.z_10, web([a.sticky, {top: 0}])]}> 121 <Layout.Header.Outer noBottomBorder> 122 <Layout.Header.BackButton /> 123 <Layout.Header.Content> 124 <Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText> 125 {author && ( 126 <Layout.Header.SubtitleText> 127 {_(msg`From @${sanitizedAuthor}`)} 128 </Layout.Header.SubtitleText> 129 )} 130 </Layout.Header.Content> 131 <Layout.Header.Slot> 132 <Button 133 label={_(msg`Share`)} 134 size="small" 135 variant="ghost" 136 color="primary" 137 shape="round" 138 onPress={onShare} 139 hitSlop={HITSLOP_10} 140 style={[{right: -3}]}> 141 <ButtonIcon icon={Share} size="md" /> 142 </Button> 143 </Layout.Header.Slot> 144 </Layout.Header.Outer> 145 <TabBar items={sections.map(section => section.title)} {...props} /> 146 </Layout.Center> 147 )} 148 initialPage={0}> 149 {sections.map((section, i) => ( 150 <View key={i}>{section.component}</View> 151 ))} 152 </Pager> 153 </Layout.Screen> 154 ) 155} 156 157function HashtagScreenTab({ 158 fullTag, 159 author, 160 sort, 161 active, 162}: { 163 fullTag: string 164 author: string | undefined 165 sort: 'top' | 'latest' 166 active: boolean 167}) { 168 const {_} = useLingui() 169 const initialNumToRender = useInitialNumToRender() 170 const [isPTR, setIsPTR] = React.useState(false) 171 const t = useTheme() 172 const {hasSession} = useSession() 173 const trackPostView = usePostViewTracking('Hashtag') 174 175 const queryParam = React.useMemo(() => { 176 if (!author) return fullTag 177 return `${fullTag} from:${author}` 178 }, [fullTag, author]) 179 180 const { 181 data, 182 isFetched, 183 isFetchingNextPage, 184 isLoading, 185 isError, 186 error, 187 refetch, 188 fetchNextPage, 189 hasNextPage, 190 } = useSearchPostsQuery({query: queryParam, sort, enabled: active}) 191 192 const posts = React.useMemo(() => { 193 return data?.pages.flatMap(page => page.posts) || [] 194 }, [data]) 195 196 const onRefresh = React.useCallback(async () => { 197 setIsPTR(true) 198 await refetch() 199 setIsPTR(false) 200 }, [refetch]) 201 202 const onEndReached = React.useCallback(() => { 203 if (isFetchingNextPage || !hasNextPage || error) return 204 fetchNextPage() 205 }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) 206 207 const closeAllActiveElements = useCloseAllActiveElements() 208 const {requestSwitchToAccount} = useLoggedOutViewControls() 209 210 const showSignIn = () => { 211 closeAllActiveElements() 212 requestSwitchToAccount({requestedAccount: 'none'}) 213 } 214 215 const showCreateAccount = () => { 216 closeAllActiveElements() 217 requestSwitchToAccount({requestedAccount: 'new'}) 218 } 219 220 if (!hasSession) { 221 return ( 222 <SearchError 223 title={_(msg`Search is currently unavailable when logged out`)}> 224 <Text style={[a.text_md, a.text_center, a.leading_snug]}> 225 <Trans> 226 <InlineLinkText 227 label={_(msg`Sign in`)} 228 to={'#'} 229 onPress={showSignIn}> 230 Sign in 231 </InlineLinkText> 232 <Text style={t.atoms.text_contrast_medium}> or </Text> 233 <InlineLinkText 234 label={_(msg`Create an account`)} 235 to={'#'} 236 onPress={showCreateAccount}> 237 create an account 238 </InlineLinkText> 239 <Text> </Text> 240 <Text style={t.atoms.text_contrast_medium}> 241 to search for news, sports, politics, and everything else 242 happening on Bluesky. 243 </Text> 244 </Trans> 245 </Text> 246 </SearchError> 247 ) 248 } 249 250 return ( 251 <> 252 {posts.length < 1 ? ( 253 <ListMaybePlaceholder 254 isLoading={isLoading || !isFetched} 255 isError={isError} 256 onRetry={refetch} 257 emptyType="results" 258 emptyMessage={_(msg`We couldn't find any results for that hashtag.`)} 259 /> 260 ) : ( 261 <List 262 data={posts} 263 renderItem={renderItem} 264 keyExtractor={keyExtractor} 265 refreshing={isPTR} 266 onRefresh={onRefresh} 267 onEndReached={onEndReached} 268 onEndReachedThreshold={4} 269 onItemSeen={trackPostView} 270 // @ts-ignore web only -prf 271 desktopFixedHeight 272 ListFooterComponent={ 273 <ListFooter 274 isFetchingNextPage={isFetchingNextPage} 275 error={cleanError(error)} 276 onRetry={fetchNextPage} 277 /> 278 } 279 initialNumToRender={initialNumToRender} 280 windowSize={11} 281 /> 282 )} 283 </> 284 ) 285}