diff --git a/src/App.native.tsx b/src/App.native.tsx index 39ab7ca92c..780295ddce 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -57,6 +57,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' +import {Provider as TrendingConfigProvider} from '#/state/trending-config' import {TestCtrls} from '#/view/com/testing/TestCtrls' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' @@ -143,12 +144,14 @@ function InnerApp() { - - - - - + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 8d13a826e7..8a2e13600f 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -47,6 +47,7 @@ import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' import {Provider as StarterPackProvider} from '#/state/shell/starter-pack' import {Provider as HiddenRepliesProvider} from '#/state/threadgate-hidden-replies' +import {Provider as TrendingConfigProvider} from '#/state/trending-config' import {Provider as ActiveVideoProvider} from '#/view/com/util/post-embeds/ActiveVideoWebContext' import {Provider as VideoVolumeProvider} from '#/view/com/util/post-embeds/VideoVolumeContext' import * as Toast from '#/view/com/util/Toast' @@ -127,8 +128,10 @@ function InnerApp() { - - + + + + diff --git a/src/components/TrendingTopics.tsx b/src/components/TrendingTopics.tsx index e765f03ea4..41c086b83c 100644 --- a/src/components/TrendingTopics.tsx +++ b/src/components/TrendingTopics.tsx @@ -91,7 +91,6 @@ export function TrendingTopic({ + {children} ) diff --git a/src/components/interstitials/Trending.tsx b/src/components/interstitials/Trending.tsx index 2dd2c84af2..a19d1dfd80 100644 --- a/src/components/interstitials/Trending.tsx +++ b/src/components/interstitials/Trending.tsx @@ -2,26 +2,44 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useTrendingSettingsApi} from '#/state/preferences/trending' +import { + useTrendingSettings, + useTrendingSettingsApi, +} 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 {Button, ButtonIcon} from '#/components/Button' import {GradientFill} from '#/components/GradientFill' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {Trending2_Stroke2_Corner2_Rounded as Graph} from '#/components/icons/Trending2' import * as Prompt from '#/components/Prompt' -import {TrendingTopic, TrendingTopicLink} from '#/components/TrendingTopics' +import { + TrendingTopic, + TrendingTopicLink, + TrendingTopicSkeleton, +} from '#/components/TrendingTopics' import {Text} from '#/components/Typography' -import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' export function TrendingInterstitial() { + const {enabled} = useTrendingConfig() + const {trendingDiscoverHidden} = useTrendingSettings() + return !enabled ? null : trendingDiscoverHidden ? null : +} + +export function Inner() { const t = useTheme() const {_} = useLingui() const gutters = useGutters(['base']) const trendingPrompt = Prompt.usePromptControl() - const {data: topics, error, isLoading} = useTrendingTopics() const {setTrendingDiscoverHidden} = useTrendingSettingsApi() + const {data: trending, error, isLoading} = useTrendingTopics() + const noTopics = !isLoading && !error && !trending?.topics - return ( + return error || noTopics ? null : ( - + Trending - BETA + + BETA + @@ -54,9 +74,13 @@ export function TrendingInterstitial() { - {isLoading ? null : error || !topics ? null : ( + {isLoading ? ( + Array(TRENDING_TOPICS_COUNT) + .fill(0) + .map((_n, i) => ) + ) : !trending?.topics ? null : ( <> - {topics.map(topic => ( + {trending.topics.map(topic => ( {({hovered}) => ( : null +} + +function Inner() { const t = useTheme() const gutters = useGutters([0, 'compact']) - const {data: topics, error, isLoading} = useTrendingTopics() + const {data: trending, error, isLoading} = useTrendingTopics() + const noTopics = !isLoading && !error && !trending?.topics - return ( - - {isLoading ? null : error || !topics ? null : ( - <> - {topics.map(topic => ( - - {({hovered}) => ( - - )} - - ))} - - )} - + return error || noTopics ? null : ( + <> + + + + + + Trending + + + + + BETA + + + + + What people are posting about now + + + + + + + {isLoading ? ( + Array(TRENDING_TOPICS_COUNT) + .fill(0) + .map((_, i) => ) + ) : !trending?.topics ? null : ( + <> + {trending.topics.map(topic => ( + + {({hovered}) => ( + + )} + + ))} + + )} + + + ) } diff --git a/src/state/queries/service-config.ts b/src/state/queries/service-config.ts new file mode 100644 index 0000000000..8540227e3e --- /dev/null +++ b/src/state/queries/service-config.ts @@ -0,0 +1,35 @@ +import {useQuery} from '@tanstack/react-query' + +import {useAgent} from '#/state/session' + +type ServiceConfig = { + checkEmailConfirmed: boolean + trendingTopicsEnabled: boolean + trendingTopicsLangs: string[] +} + +export function useServiceConfigQuery() { + const agent = useAgent() + return useQuery({ + queryKey: ['service-config'], + queryFn: async () => { + try { + const {data} = await agent.api.app.bsky.unspecced.getConfig() + return { + checkEmailConfirmed: Boolean(data.checkEmailConfirmed), + // TODO + trendingTopicsEnabled: true, // Boolean(data.trendingTopicsEnabled), + // TODO + trendingTopicsLangs: ['en'], // data.trendingTopicsLangs ?? [], + } + } catch (e) { + return { + checkEmailConfirmed: false, + trendingTopicsEnabled: false, + trendingTopicsLangs: [], + } + } + }, + staleTime: 5 * 60 * 1000, + }) +} diff --git a/src/state/queries/trending/useTrendingTopics.ts b/src/state/queries/trending/useTrendingTopics.ts index 6bae41b03d..ad2cfa220c 100644 --- a/src/state/queries/trending/useTrendingTopics.ts +++ b/src/state/queries/trending/useTrendingTopics.ts @@ -10,14 +10,38 @@ export type TrendingTopic = { link: string } -export const trendingTopicsQueryKey = ['trending-topics'] +export const DEFAULT_LIMIT = 12 -// TODO ideally handle down-state faster +export const trendingTopicsQueryKey = ['trending-topics'] export function useTrendingTopics() { return useQuery({ queryKey: trendingTopicsQueryKey, async queryFn() { + /* + try { + const params = new URLSearchParams() + params.set('viewer', agent.session?.did || '') + const res = await fetch( + ``, + { + headers: { + Authorization: + 'Bearer ', + }, + }, + ) + + if (!res.ok) { + throw new Error('Failed to fetch trending topics') + } + + const data = await res.json() + return data.topics + } catch (e) { + console.error(e) + } + */ const topics: TrendingTopic[] = [ { topic: '#atproto', @@ -50,7 +74,10 @@ export function useTrendingTopics() { link: 'at://did:plc:vpkhqolt662uhesyj6nxm7ys/app.bsky.feed.generator/infreq', }, ] - return topics + return { + topics, + recommended: [], + } }, }) } diff --git a/src/state/shell/trending-config.tsx b/src/state/shell/trending-config.tsx new file mode 100644 index 0000000000..ccb4357066 --- /dev/null +++ b/src/state/shell/trending-config.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +import {useLanguagePrefs} from '#/state/preferences/languages' +import {useServiceConfigQuery} from '#/state/queries/service-config' + +type Context = { + enabled: boolean +} + +const Context = React.createContext({ + enabled: false, +}) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const langPrefs = useLanguagePrefs() + const {data: config} = useServiceConfigQuery() + const ctx = React.useMemo(() => { + const serviceEnabled = Boolean(config?.trendingTopicsEnabled) + const languageIsSupported = langPrefs.contentLanguages.some(lang => { + return (config?.trendingTopicsLangs ?? []).includes(lang) + }) + return { + enabled: serviceEnabled && languageIsSupported, + } + }, [config, langPrefs]) + return {children} +} + +export function useTrendingConfig() { + return React.useContext(Context) +} diff --git a/src/state/trending-config.tsx b/src/state/trending-config.tsx new file mode 100644 index 0000000000..22d8829876 --- /dev/null +++ b/src/state/trending-config.tsx @@ -0,0 +1,32 @@ +import React from 'react' + +import {useLanguagePrefs} from '#/state/preferences/languages' +import {useServiceConfigQuery} from '#/state/queries/service-config' + +type Context = { + enabled: boolean +} + +const Context = React.createContext({ + enabled: false, +}) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const langPrefs = useLanguagePrefs() + const {data: config} = useServiceConfigQuery() + const ctx = React.useMemo(() => { + // TODO maybe default to true + const serviceEnabled = Boolean(config?.trendingTopicsEnabled) + const languageIsSupported = langPrefs.contentLanguages.some(lang => { + return (config?.trendingTopicsLangs ?? []).includes(lang) + }) + return { + enabled: serviceEnabled && languageIsSupported, + } + }, [config, langPrefs]) + return {children} +} + +export function useTrendingConfig() { + return React.useContext(Context) +} diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index 2ce2f923f0..14a7f4348a 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -25,21 +25,18 @@ import { ProfileCardFeedLoadingPlaceholder, } from '#/view/com/util/LoadingPlaceholder' import {UserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, tokens,useTheme, ViewStyleProp} from '#/alf' +import {ExploreTrendingTopics} from '#/screens/Search/components/ExploreTrendingTopics' +import {atoms as a, useTheme, ViewStyleProp} from '#/alf' import {Button} from '#/components/Button' import * as FeedCard from '#/components/FeedCard' -import {GradientFill} from '#/components/GradientFill' import {ArrowBottom_Stroke2_Corner0_Rounded as ArrowBottom} from '#/components/icons/Arrow' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {Props as SVGIconProps} from '#/components/icons/common' import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle' -import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2' import {UserCircle_Stroke2_Corner0_Rounded as Person} from '#/components/icons/UserCircle' import {Loader} from '#/components/Loader' import * as StarterPackSuggestions from '#/components/StarterPack/Suggestions' -import * as TrendingTopics from '#/components/TrendingTopics' import {Text} from '#/components/Typography' -import {ExploreTrendingTopics} from "#/screens/Search/components/ExploreTrendingTopics" function SuggestedItemsHeader({ title, @@ -251,7 +248,6 @@ type ExploreScreenItems = | { type: 'trendingTopics' key: string - topics: string[] } | { type: 'suggestedStarterPacks' @@ -349,89 +345,8 @@ export function Explore() { i.push({ type: 'trendingTopics', key: `trending-topics`, - topics: [ - '#atproto', - 'South Korea', - 'Wired', - 'Basket Weaving', - 'Coup', - 'Chappel Roan', - 'the juice', - 'Superman', - '#FCF', - 'Open Web', - ], }) - /* - i.push({ - type: 'header', - key: 'starter-packs-header', - title: _(msg`Starter packs`), - description: _(msg`Find accounts to follow based on your interests.`), - icon: PersonGroup, - }) - i.push({ - type: 'suggestedStarterPacks', - key: 'suggested-starter-packs', - starterPacks: [ - { - uri: 'at://did:plc:ragtjsm2j2vknwkz3zp4oxrd/app.bsky.graph.starterpack/3kvaphb2hpf2u', - cid: '', - indexedAt: '', - record: { - $type: 'app.bsky.graph.starterpack', - description: - 'Engineers who work on or discuss the ATProtocol, both at Bluesky and in the community', - list: 'at://did:plc:ragtjsm2j2vknwkz3zp4oxrd/app.bsky.graph.list/3kvaphagg4e2l', - name: 'ATProtocol Hackers', - }, - creator: { - did: 'did:plc:ragtjsm2j2vknwkz3zp4oxrd', - handle: 'pfrazee.com', - displayName: 'Paul Frazee', - }, - }, - { - uri: 'at://did:plc:qrllvid7s54k4hnwtqxwetrf/app.bsky.graph.starterpack/3kvuh7u324m2c', - cid: '', - indexedAt: '', - record: { - $type: 'app.bsky.graph.starterpack', - createdAt: '2024-06-26T23:23:01.073Z', - description: - 'Legal practice, scholarship, journalism, and argument', - feeds: [], - list: 'at://did:plc:qrllvid7s54k4hnwtqxwetrf/app.bsky.graph.list/3kvuh7twnjc2m', - name: 'Bluesky for Law', - }, - creator: { - did: 'did:plc:qrllvid7s54k4hnwtqxwetrf', - handle: 'joshuajfriedman.com', - displayName: 'Joshua J. Friedman', - }, - }, - { - uri: 'at://did:plc:7exlcsle4mjfhu3wnhcgizz6/app.bsky.graph.starterpack/3laotg2fqva2h', - cid: '', - indexedAt: '', - record: { - $type: 'app.bsky.graph.starterpack', - createdAt: '2024-11-11T17:22:22.743Z', - description: 'people who work at the website, the verge dot com', - list: 'at://did:plc:7exlcsle4mjfhu3wnhcgizz6/app.bsky.graph.list/3laotg2bjok2d', - name: 'The Verge staff', - }, - creator: { - did: 'did:plc:7exlcsle4mjfhu3wnhcgizz6', - handle: 'theverge.com', - displayName: 'The Verge', - }, - }, - ], - }) - */ - i.push({ type: 'header', key: 'suggested-follows-header', @@ -596,25 +511,7 @@ export function Explore() { ) } case 'trendingTopics': { - return ( - <> - - - BETA - - } - /> - - - - - ) + return } case 'suggestedStarterPacks': { return ( @@ -696,7 +593,7 @@ export function Explore() { } } }, - [_, t, moderationOpts], + [t, moderationOpts], ) // note: actually not a screen, instead it's nested within diff --git a/src/view/shell/desktop/SidebarTrendingTopics.tsx b/src/view/shell/desktop/SidebarTrendingTopics.tsx index 28f47d9810..8ca9b63f4e 100644 --- a/src/view/shell/desktop/SidebarTrendingTopics.tsx +++ b/src/view/shell/desktop/SidebarTrendingTopics.tsx @@ -6,7 +6,11 @@ import { useTrendingSettings, useTrendingSettingsApi, } from '#/state/preferences/trending' -import {useTrendingTopics} from '#/state/queries/trending/useTrendingTopics' +import { + DEFAULT_LIMIT as TRENDING_TOPICS_COUNT, + useTrendingTopics, +} from '#/state/queries/trending/useTrendingTopics' +import {useTrendingConfig} from '#/state/trending-config' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {Divider} from '#/components/Divider' @@ -21,8 +25,9 @@ import { import {Text} from '#/components/Typography' export function SidebarTrendingTopics() { + const {enabled} = useTrendingConfig() const {trendingSidebarHidden} = useTrendingSettings() - return trendingSidebarHidden ? null : + return !enabled ? null : trendingSidebarHidden ? null : } function Inner() { @@ -30,9 +35,8 @@ function Inner() { const {_} = useLingui() const trendingPrompt = Prompt.usePromptControl() const {setTrendingSidebarHidden} = useTrendingSettingsApi() - - const {data: topics, error, isLoading} = useTrendingTopics() - const noTopics = !isLoading && !error && !topics + const {data: trending, error, isLoading} = useTrendingTopics() + const noTopics = !isLoading && !error && !trending?.topics return error || noTopics ? null : ( <> @@ -61,12 +65,14 @@ function Inner() { {isLoading ? ( - Array(8) + Array(TRENDING_TOPICS_COUNT) .fill(0) - .map((_, i) => ) - ) : error || !topics ? null : ( + .map((_n, i) => ( + + )) + ) : !trending?.topics ? null : ( <> - {topics.map(topic => ( + {trending.topics.map(topic => ( {({hovered}) => (