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

Configure Feed

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

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