diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 7443128d2c..18705c5ffb 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -100,6 +100,7 @@ import {LanguageSettingsScreen} from './screens/Settings/LanguageSettings' import {PrivacyAndSecuritySettingsScreen} from './screens/Settings/PrivacyAndSecuritySettings' import {SettingsScreen} from './screens/Settings/Settings' import {ThreadPreferencesScreen} from './screens/Settings/ThreadPreferences' +import TopicScreen from './screens/Topic' const navigationRef = createNavigationContainerRef() @@ -376,6 +377,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { getComponent={() => HashtagScreen} options={{title: title(msg`Hashtag`)}} /> + TopicScreen} + options={{title: title(msg`Topic`)}} + /> MessagesConversationScreen} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 238e4be4c3..d720886e9f 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -47,6 +47,7 @@ export type CommonNavigatorParams = { AppIconSettings: undefined Search: {q?: string} Hashtag: {tag: string; author?: string} + Topic: {topic: string} MessagesConversation: {conversation: string; embed?: string} MessagesSettings: undefined NotificationSettings: undefined @@ -92,6 +93,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Feeds: undefined Notifications: undefined Hashtag: {tag: string; author?: string} + Topic: {topic: string} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} } @@ -105,6 +107,7 @@ export type AllNavigatorParams = CommonNavigatorParams & { Notifications: undefined MyProfileTab: undefined Hashtag: {tag: string; author?: string} + Topic: {topic: string} MessagesTab: undefined Messages: {animation?: 'push' | 'pop'} Start: {name: string; rkey: string} diff --git a/src/routes.ts b/src/routes.ts index 188665d849..7cd7c0880d 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -53,6 +53,7 @@ export const router = new Router({ CopyrightPolicy: '/support/copyright', // hashtags Hashtag: '/hashtag/:tag', + Topic: '/topic/:topic', // DMs Messages: '/messages', MessagesSettings: '/messages/settings', diff --git a/src/screens/Topic.tsx b/src/screens/Topic.tsx new file mode 100644 index 0000000000..6cd69f05f2 --- /dev/null +++ b/src/screens/Topic.tsx @@ -0,0 +1,204 @@ +import React from 'react' +import {ListRenderItemInfo, View} from 'react-native' +import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps} from '@react-navigation/native-stack' + +import {HITSLOP_10} from '#/lib/constants' +import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {CommonNavigatorParams} from '#/lib/routes/types' +import {shareUrl} from '#/lib/sharing' +import {cleanError} from '#/lib/strings/errors' +import {enforceLen} from '#/lib/strings/helpers' +import {useSearchPostsQuery} from '#/state/queries/search-posts' +import {useSetMinimalShellMode} from '#/state/shell' +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, web} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' +import * as Layout from '#/components/Layout' +import {ListFooter, ListMaybePlaceholder} from '#/components/Lists' + +const renderItem = ({item}: ListRenderItemInfo) => { + return +} + +const keyExtractor = (item: PostView, index: number) => { + return `${item.uri}-${index}` +} + +export default function TopicScreen({ + route, +}: NativeStackScreenProps) { + const {topic} = route.params + const {_} = useLingui() + + const headerTitle = React.useMemo(() => { + return enforceLen(decodeURIComponent(topic), 24, true, 'middle') + }, [topic]) + + const onShare = React.useCallback(() => { + const url = new URL('https://bsky.app') + url.pathname = `/topic/${topic}` + shareUrl(url.toString()) + }, [topic]) + + const [activeTab, setActiveTab] = React.useState(0) + const setMinimalShellMode = useSetMinimalShellMode() + + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) + + const onPageSelected = React.useCallback( + (index: number) => { + setMinimalShellMode(false) + setActiveTab(index) + }, + [setMinimalShellMode], + ) + + const sections = React.useMemo(() => { + return [ + { + title: _(msg`Top`), + component: ( + + ), + }, + { + title: _(msg`Latest`), + component: ( + + ), + }, + ] + }, [_, topic, activeTab]) + + return ( + + + + + {headerTitle} + + + + + + ( + + section.title)} {...props} /> + + )} + initialPage={0}> + {sections.map((section, i) => ( + {section.component} + ))} + + + ) +} + +function TopicScreenTab({ + topic, + sort, + active, +}: { + topic: string + sort: 'top' | 'latest' + active: boolean +}) { + const {_} = useLingui() + const initialNumToRender = useInitialNumToRender() + const [isPTR, setIsPTR] = React.useState(false) + + const { + data, + isFetched, + isFetchingNextPage, + isLoading, + isError, + error, + refetch, + fetchNextPage, + hasNextPage, + } = useSearchPostsQuery({ + query: decodeURIComponent(topic), + sort, + enabled: active, + }) + + const posts = React.useMemo(() => { + return data?.pages.flatMap(page => page.posts) || [] + }, [data]) + + const onRefresh = React.useCallback(async () => { + setIsPTR(true) + await refetch() + setIsPTR(false) + }, [refetch]) + + const onEndReached = React.useCallback(() => { + if (isFetchingNextPage || !hasNextPage || error) return + fetchNextPage() + }, [isFetchingNextPage, hasNextPage, error, fetchNextPage]) + + return ( + <> + {posts.length < 1 ? ( + + ) : ( + + } + initialNumToRender={initialNumToRender} + windowSize={11} + /> + )} + + ) +}