import {useCallback, useMemo, useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' import {type AppBskyFeedDefs} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {HITSLOP_10} from '#/lib/constants' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' import {type CommonNavigatorParams} from '#/lib/routes/types' import {shareUrl} from '#/lib/sharing' import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {enforceLen} from '#/lib/strings/helpers' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' import {useSearchPostsQuery} from '#/state/queries/search-posts' import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' import {Pager} from '#/view/com/pager/Pager' import {TabBar} from '#/view/com/pager/TabBar' import {Post} from '#/view/com/post/Post' import {List} from '#/view/com/util/List' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {ArrowOutOfBoxModified_Stroke2_Corner2_Rounded as Share} from '#/components/icons/ArrowOutOfBox' import * as Layout from '#/components/Layout' import {InlineLinkText} from '#/components/Link' import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' import {SearchError} from '#/components/SearchError' import {Text} from '#/components/Typography' const renderItem = ({item}: ListRenderItemInfo) => { return } const keyExtractor = (item: AppBskyFeedDefs.PostView, index: number) => { return `${item.uri}-${index}` } export default function HashtagScreen({ route, }: NativeStackScreenProps) { const {tag, author} = route.params const {_} = useLingui() const decodedTag = useMemo(() => { return decodeURIComponent(tag) }, [tag]) const isCashtag = decodedTag.startsWith('$') const fullTag = useMemo(() => { // Cashtags already include the $ prefix, hashtags need # added return isCashtag ? decodedTag : `#${decodedTag}` }, [decodedTag, isCashtag]) const headerTitle = useMemo(() => { // Keep cashtags uppercase, lowercase hashtags const displayTag = isCashtag ? fullTag.toUpperCase() : fullTag.toLowerCase() return enforceLen(displayTag, 24, true, 'middle') }, [fullTag, isCashtag]) const sanitizedAuthor = useMemo(() => { if (!author) return '' return sanitizeHandle(author) }, [author]) const onShare = useCallback(() => { const url = new URL('https://witchsky.app') url.pathname = `/hashtag/${decodeURIComponent(tag)}` if (author) { url.searchParams.set('author', author) } shareUrl(url.toString()) }, [tag, author]) const [activeTab, setActiveTab] = useState(0) const enableSquareButtons = useEnableSquareButtons() const onPageSelected = (index: number) => { setActiveTab(index) } const sections = useMemo(() => { return [ { title: _(msg`Top`), component: ( ), }, { title: _(msg`Latest`), component: ( ), }, ] }, [_, fullTag, author, activeTab]) return ( ( {headerTitle} {author && ( {_(msg`From @${sanitizedAuthor}`)} )} section.title)} {...props} /> )} initialPage={0}> {sections.map((section, i) => ( {section.component} ))} ) } function HashtagScreenTab({ fullTag, author, sort, active, }: { fullTag: string author: string | undefined sort: 'top' | 'latest' active: boolean }) { const {_} = useLingui() const initialNumToRender = useInitialNumToRender() const [isPTR, setIsPTR] = useState(false) const t = useTheme() const {hasSession} = useSession() const trackPostView = usePostViewTracking('Hashtag') const isCashtag = fullTag.startsWith('$') const queryParam = useMemo(() => { // Cashtags need # prefix for search: "#$BTC" or "#$BTC from:author" const searchTag = isCashtag ? `#${fullTag}` : fullTag if (!author) return searchTag return `${searchTag} from:${author}` }, [fullTag, author, isCashtag]) const { data, isFetched, isFetchingNextPage, isLoading, isError, error, refetch, fetchNextPage, hasNextPage, } = useSearchPostsQuery({query: queryParam, sort, enabled: active}) const posts = useMemo(() => { return data?.pages.flatMap(page => page.posts) || [] }, [data]) const onRefresh = useCallback(async () => { setIsPTR(true) await refetch() setIsPTR(false) }, [refetch]) const onEndReached = useCallback(() => { if (isFetchingNextPage || !hasNextPage || error) return fetchNextPage() }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) const closeAllActiveElements = useCloseAllActiveElements() const {requestSwitchToAccount} = useLoggedOutViewControls() const showSignIn = () => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'none'}) } const showCreateAccount = () => { closeAllActiveElements() requestSwitchToAccount({requestedAccount: 'new'}) } if (!hasSession) { return ( Sign in or create an account to search for news, sports, politics, and everything else happening on Bluesky. ) } return ( <> {posts.length < 1 ? ( ) : ( } initialNumToRender={initialNumToRender} windowSize={11} /> )} ) }