diff options
Diffstat (limited to 'src/screens')
-rw-r--r-- | src/screens/Search/components/ExploreRecommendations.tsx | 95 | ||||
-rw-r--r-- | src/screens/Search/components/ExploreTrendingTopics.tsx | 102 | ||||
-rw-r--r-- | src/screens/Settings/ContentAndMediaSettings.tsx | 27 | ||||
-rw-r--r-- | src/screens/Topic.tsx | 204 |
4 files changed, 428 insertions, 0 deletions
diff --git a/src/screens/Search/components/ExploreRecommendations.tsx b/src/screens/Search/components/ExploreRecommendations.tsx new file mode 100644 index 000000000..e253cfb5a --- /dev/null +++ b/src/screens/Search/components/ExploreRecommendations.tsx @@ -0,0 +1,95 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {isWeb} from '#/platform/detection' +import {useTrendingSettings} from '#/state/preferences/trending' +import { + DEFAULT_LIMIT as RECOMMENDATIONS_COUNT, + useTrendingTopics, +} from '#/state/queries/trending/useTrendingTopics' +import {useTrendingConfig} from '#/state/trending-config' +import {atoms as a, useGutters, useTheme} from '#/alf' +import {Hashtag_Stroke2_Corner0_Rounded} from '#/components/icons/Hashtag' +import { + TrendingTopic, + TrendingTopicLink, + TrendingTopicSkeleton, +} from '#/components/TrendingTopics' +import {Text} from '#/components/Typography' + +export function ExploreRecommendations() { + const {enabled} = useTrendingConfig() + const {trendingDisabled} = useTrendingSettings() + return enabled && !trendingDisabled ? <Inner /> : null +} + +function Inner() { + const t = useTheme() + const gutters = useGutters([0, 'compact']) + const {data: trending, error, isLoading} = useTrendingTopics() + const noRecs = !isLoading && !error && !trending?.suggested?.length + + return error || noRecs ? null : ( + <> + <View + style={[ + isWeb + ? [a.flex_row, a.px_lg, a.py_lg, a.pt_2xl, a.gap_md] + : [{flexDirection: 'row-reverse'}, a.p_lg, a.pt_2xl, a.gap_md], + a.border_b, + t.atoms.border_contrast_low, + ]}> + <View style={[a.flex_1, a.gap_sm]}> + <View style={[a.flex_row, a.align_center, a.gap_sm]}> + <Hashtag_Stroke2_Corner0_Rounded + size="lg" + fill={t.palette.primary_500} + style={{marginLeft: -2}} + /> + <Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}> + <Trans>Recommended</Trans> + </Text> + </View> + <Text style={[t.atoms.text_contrast_high, a.leading_snug]}> + <Trans>Feeds we think you might like.</Trans> + </Text> + </View> + </View> + + <View style={[a.pt_md, a.pb_lg]}> + <View + style={[ + a.flex_row, + a.justify_start, + a.flex_wrap, + {rowGap: 8, columnGap: 6}, + gutters, + ]}> + {isLoading ? ( + Array(RECOMMENDATIONS_COUNT) + .fill(0) + .map((_, i) => <TrendingTopicSkeleton key={i} index={i} />) + ) : !trending?.suggested ? null : ( + <> + {trending.suggested.map(topic => ( + <TrendingTopicLink key={topic.link} topic={topic}> + {({hovered}) => ( + <TrendingTopic + topic={topic} + style={[ + hovered && [ + t.atoms.border_contrast_high, + t.atoms.bg_contrast_25, + ], + ]} + /> + )} + </TrendingTopicLink> + ))} + </> + )} + </View> + </View> + </> + ) +} diff --git a/src/screens/Search/components/ExploreTrendingTopics.tsx b/src/screens/Search/components/ExploreTrendingTopics.tsx new file mode 100644 index 000000000..be347dcd4 --- /dev/null +++ b/src/screens/Search/components/ExploreTrendingTopics.tsx @@ -0,0 +1,102 @@ +import {View} from 'react-native' +import {Trans} from '@lingui/macro' + +import {isWeb} from '#/platform/detection' +import {useTrendingSettings} from '#/state/preferences/trending' +import { + DEFAULT_LIMIT as TRENDING_TOPICS_COUNT, + useTrendingTopics, +} from '#/state/queries/trending/useTrendingTopics' +import {useTrendingConfig} from '#/state/trending-config' +import {atoms as a, tokens, useGutters, useTheme} from '#/alf' +import {GradientFill} from '#/components/GradientFill' +import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2' +import { + TrendingTopic, + TrendingTopicLink, + TrendingTopicSkeleton, +} from '#/components/TrendingTopics' +import {Text} from '#/components/Typography' + +export function ExploreTrendingTopics() { + const {enabled} = useTrendingConfig() + const {trendingDisabled} = useTrendingSettings() + return enabled && !trendingDisabled ? <Inner /> : null +} + +function Inner() { + const t = useTheme() + const gutters = useGutters([0, 'compact']) + const {data: trending, error, isLoading} = useTrendingTopics() + const noTopics = !isLoading && !error && !trending?.topics?.length + + return error || noTopics ? null : ( + <> + <View + style={[ + isWeb + ? [a.flex_row, a.px_lg, a.py_lg, a.pt_2xl, a.gap_md] + : [{flexDirection: 'row-reverse'}, a.p_lg, a.pt_2xl, a.gap_md], + a.border_b, + t.atoms.border_contrast_low, + ]}> + <View style={[a.flex_1, a.gap_sm]}> + <View style={[a.flex_row, a.align_center, a.gap_sm]}> + <Trending + size="lg" + fill={t.palette.primary_500} + style={{marginLeft: -2}} + /> + <Text style={[a.text_2xl, a.font_heavy, t.atoms.text]}> + <Trans>Trending</Trans> + </Text> + <View style={[a.py_xs, a.px_sm, a.rounded_sm, a.overflow_hidden]}> + <GradientFill gradient={tokens.gradients.primary} /> + <Text style={[a.text_sm, a.font_heavy, {color: 'white'}]}> + <Trans>BETA</Trans> + </Text> + </View> + </View> + <Text style={[t.atoms.text_contrast_high, a.leading_snug]}> + <Trans>What people are posting about.</Trans> + </Text> + </View> + </View> + + <View style={[a.pt_md, a.pb_lg]}> + <View + style={[ + a.flex_row, + a.justify_start, + a.flex_wrap, + {rowGap: 8, columnGap: 6}, + gutters, + ]}> + {isLoading ? ( + Array(TRENDING_TOPICS_COUNT) + .fill(0) + .map((_, i) => <TrendingTopicSkeleton key={i} index={i} />) + ) : !trending?.topics ? null : ( + <> + {trending.topics.map(topic => ( + <TrendingTopicLink key={topic.link} topic={topic}> + {({hovered}) => ( + <TrendingTopic + topic={topic} + style={[ + hovered && [ + t.atoms.border_contrast_high, + t.atoms.bg_contrast_25, + ], + ]} + /> + )} + </TrendingTopicLink> + ))} + </> + )} + </View> + </View> + </> + ) +} diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index 17f8fa506..bdbe1d191 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -9,6 +9,11 @@ import { useInAppBrowser, useSetInAppBrowser, } from '#/state/preferences/in-app-browser' +import { + useTrendingSettings, + useTrendingSettingsApi, +} from '#/state/preferences/trending' +import {useTrendingConfig} from '#/state/trending-config' import * as SettingsList from '#/screens/Settings/components/SettingsList' import * as Toggle from '#/components/forms/Toggle' import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble' @@ -16,6 +21,7 @@ import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home' import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh' import {Play_Stroke2_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' +import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' import {Window_Stroke2_Corner2_Rounded as WindowIcon} from '#/components/icons/Window' import * as Layout from '#/components/Layout' @@ -29,6 +35,9 @@ export function ContentAndMediaSettingsScreen({}: Props) { const setAutoplayDisabledPref = useSetAutoplayDisabled() const inAppBrowserPref = useInAppBrowser() const setUseInAppBrowser = useSetInAppBrowser() + const {enabled: trendingEnabled} = useTrendingConfig() + const {trendingDisabled} = useTrendingSettings() + const {setTrendingDisabled} = useTrendingSettingsApi() return ( <Layout.Screen> @@ -104,6 +113,24 @@ export function ContentAndMediaSettingsScreen({}: Props) { <Toggle.Platform /> </SettingsList.Item> </Toggle.Item> + {trendingEnabled && ( + <> + <SettingsList.Divider /> + <Toggle.Item + name="show_trending_topics" + label={_(msg`Enable trending topics`)} + value={!trendingDisabled} + onChange={value => setTrendingDisabled(!value)}> + <SettingsList.Item> + <SettingsList.ItemIcon icon={Graph} /> + <SettingsList.ItemText> + <Trans>Enable trending topics</Trans> + </SettingsList.ItemText> + <Toggle.Platform /> + </SettingsList.Item> + </Toggle.Item> + </> + )} </SettingsList.Container> </Layout.Content> </Layout.Screen> diff --git a/src/screens/Topic.tsx b/src/screens/Topic.tsx new file mode 100644 index 000000000..6cd69f05f --- /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<PostView>) => { + return <Post post={item} /> +} + +const keyExtractor = (item: PostView, index: number) => { + return `${item.uri}-${index}` +} + +export default function TopicScreen({ + route, +}: NativeStackScreenProps<CommonNavigatorParams, 'Topic'>) { + 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: ( + <TopicScreenTab topic={topic} sort="top" active={activeTab === 0} /> + ), + }, + { + title: _(msg`Latest`), + component: ( + <TopicScreenTab + topic={topic} + sort="latest" + active={activeTab === 1} + /> + ), + }, + ] + }, [_, topic, activeTab]) + + return ( + <Layout.Screen> + <Layout.Header.Outer noBottomBorder> + <Layout.Header.BackButton /> + <Layout.Header.Content> + <Layout.Header.TitleText>{headerTitle}</Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot> + <Button + label={_(msg`Share`)} + size="small" + variant="ghost" + color="primary" + shape="round" + onPress={onShare} + hitSlop={HITSLOP_10} + style={[{right: -3}]}> + <ButtonIcon icon={Share} size="md" /> + </Button> + </Layout.Header.Slot> + </Layout.Header.Outer> + <Pager + onPageSelected={onPageSelected} + renderTabBar={props => ( + <Layout.Center style={[a.z_10, web([a.sticky, {top: 0}])]}> + <TabBar items={sections.map(section => section.title)} {...props} /> + </Layout.Center> + )} + initialPage={0}> + {sections.map((section, i) => ( + <View key={i}>{section.component}</View> + ))} + </Pager> + </Layout.Screen> + ) +} + +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 ? ( + <ListMaybePlaceholder + isLoading={isLoading || !isFetched} + isError={isError} + onRetry={refetch} + emptyType="results" + emptyMessage={_(msg`We couldn't find any results for that topic.`)} + /> + ) : ( + <List + data={posts} + renderItem={renderItem} + keyExtractor={keyExtractor} + refreshing={isPTR} + onRefresh={onRefresh} + onEndReached={onEndReached} + onEndReachedThreshold={4} + // @ts-ignore web only -prf + desktopFixedHeight + ListFooterComponent={ + <ListFooter + isFetchingNextPage={isFetchingNextPage} + error={cleanError(error)} + onRetry={fetchNextPage} + /> + } + initialNumToRender={initialNumToRender} + windowSize={11} + /> + )} + </> + ) +} |