diff options
author | Eric Bailey <git@esb.lol> | 2024-12-17 21:45:39 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-17 19:45:39 -0800 |
commit | a2019aceec001e276272832b97ea5e2ec864c8a5 (patch) | |
tree | eaddab8a7a009650d93bb3b49c750619d98bb44d /src/state | |
parent | a07949ec8e63bae178a829f65c33fcd9622b28ec (diff) | |
download | voidsky-a2019aceec001e276272832b97ea5e2ec864c8a5.tar.zst |
Trending (Beta) (#7144)
* Add WIP UIs for trending topics and suggested starterpacks * Disable SPs for now * Improve explore treatment a bit, add some polish to cards * Add tiny option in RightNav * Add persisted option to hide trending from sidebar * Add to settings, abstract state, not updating in tab * Fix up hide/show toggle state, WITH broadcast hacK * Clean up persisted code, add new setting * Add new interstitial to Discover * Exploration * First hack at mute words * Wire up interstitial and Explore page * Align components * Some skeleton UI * Handle service config, enablement, load states, update lex contract * Centralize mute word handling * Stale time to 30m * Cache enabled value for reloads, use real data for service config * Remove broadcast hack * Remove titleChild * Gate settings too * Update package, rm langs * Add feature gate * Only english during beta period * Hook up real data * Tweak config * Straight passthrough links * Hook up prod agent * Fix no-show logic * Up config query to 5 min * Remove old file * Remove comment * Remove stray flex_1 * Make trending setting global * Quick placeholder state * Limit # in sidebar, tweak spacing * Tweak gaps * Handle hide/show of sidebar * Simplify messages * Remove interstitial * Revert "Remove interstitial" This reverts commit 1358ad47fdf7e633749340c410933b508af46c10. * Only show interstitial on mobile * Fix gap * Add explore page recommendations * [topics] add topic screen (#7149) * add topic screen * decode * fix search query * decode * add server route * Fix potential bad destructure (undefined) --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com> Co-authored-by: Dan Abramov <dan.abramov@gmail.com> Co-authored-by: Hailey <me@haileyok.com>
Diffstat (limited to 'src/state')
-rw-r--r-- | src/state/persisted/schema.ts | 2 | ||||
-rw-r--r-- | src/state/preferences/index.tsx | 5 | ||||
-rw-r--r-- | src/state/preferences/trending.tsx | 69 | ||||
-rw-r--r-- | src/state/queries/index.ts | 1 | ||||
-rw-r--r-- | src/state/queries/service-config.ts | 32 | ||||
-rw-r--r-- | src/state/queries/trending/useTrendingTopics.ts | 49 | ||||
-rw-r--r-- | src/state/trending-config.tsx | 70 |
7 files changed, 227 insertions, 1 deletions
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index f70d77463..0a9e5b2c0 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -125,6 +125,7 @@ const schema = z.object({ subtitlesEnabled: z.boolean().optional(), /** @deprecated */ mutedThreads: z.array(z.string()), + trendingDisabled: z.boolean().optional(), }) export type Schema = z.infer<typeof schema> @@ -170,6 +171,7 @@ export const defaults: Schema = { kawaii: false, hasCheckedForStarterPack: false, subtitlesEnabled: true, + trendingDisabled: false, } export function tryParse(rawData: string): Schema | undefined { diff --git a/src/state/preferences/index.tsx b/src/state/preferences/index.tsx index c7eaf2726..8530a8d0c 100644 --- a/src/state/preferences/index.tsx +++ b/src/state/preferences/index.tsx @@ -10,6 +10,7 @@ import {Provider as KawaiiProvider} from './kawaii' import {Provider as LanguagesProvider} from './languages' import {Provider as LargeAltBadgeProvider} from './large-alt-badge' import {Provider as SubtitlesProvider} from './subtitles' +import {Provider as TrendingSettingsProvider} from './trending' import {Provider as UsedStarterPacksProvider} from './used-starter-packs' export { @@ -39,7 +40,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { <AutoplayProvider> <UsedStarterPacksProvider> <SubtitlesProvider> - <KawaiiProvider>{children}</KawaiiProvider> + <TrendingSettingsProvider> + <KawaiiProvider>{children}</KawaiiProvider> + </TrendingSettingsProvider> </SubtitlesProvider> </UsedStarterPacksProvider> </AutoplayProvider> diff --git a/src/state/preferences/trending.tsx b/src/state/preferences/trending.tsx new file mode 100644 index 000000000..bf5d8f13c --- /dev/null +++ b/src/state/preferences/trending.tsx @@ -0,0 +1,69 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = { + trendingDisabled: Exclude<persisted.Schema['trendingDisabled'], undefined> +} +type ApiContext = { + setTrendingDisabled( + hidden: Exclude<persisted.Schema['trendingDisabled'], undefined>, + ): void +} + +const StateContext = React.createContext<StateContext>({ + trendingDisabled: Boolean(persisted.defaults.trendingDisabled), +}) +const ApiContext = React.createContext<ApiContext>({ + setTrendingDisabled() {}, +}) + +function usePersistedBooleanValue<T extends keyof persisted.Schema>(key: T) { + const [value, _set] = React.useState(() => { + return Boolean(persisted.get(key)) + }) + const set = React.useCallback< + (value: Exclude<persisted.Schema[T], undefined>) => void + >( + hidden => { + _set(Boolean(hidden)) + persisted.write(key, hidden) + }, + [key, _set], + ) + React.useEffect(() => { + return persisted.onUpdate(key, hidden => { + _set(Boolean(hidden)) + }) + }, [key, _set]) + + return [value, set] as const +} + +export function Provider({children}: React.PropsWithChildren<{}>) { + const [trendingDisabled, setTrendingDisabled] = + usePersistedBooleanValue('trendingDisabled') + + /* + * Context + */ + const state = React.useMemo(() => ({trendingDisabled}), [trendingDisabled]) + const api = React.useMemo( + () => ({setTrendingDisabled}), + [setTrendingDisabled], + ) + + return ( + <StateContext.Provider value={state}> + <ApiContext.Provider value={api}>{children}</ApiContext.Provider> + </StateContext.Provider> + ) +} + +export function useTrendingSettings() { + return React.useContext(StateContext) +} + +export function useTrendingSettingsApi() { + return React.useContext(ApiContext) +} diff --git a/src/state/queries/index.ts b/src/state/queries/index.ts index 0635bf316..d4b9d94c4 100644 --- a/src/state/queries/index.ts +++ b/src/state/queries/index.ts @@ -6,6 +6,7 @@ export const STALE = { MINUTES: { ONE: 1e3 * 60, FIVE: 1e3 * 60 * 5, + THIRTY: 1e3 * 60 * 30, }, HOURS: { ONE: 1e3 * 60 * 60, diff --git a/src/state/queries/service-config.ts b/src/state/queries/service-config.ts new file mode 100644 index 000000000..9a9db7865 --- /dev/null +++ b/src/state/queries/service-config.ts @@ -0,0 +1,32 @@ +import {useQuery} from '@tanstack/react-query' + +import {STALE} from '#/state/queries' +import {useAgent} from '#/state/session' + +type ServiceConfig = { + checkEmailConfirmed: boolean + topicsEnabled: boolean +} + +export function useServiceConfigQuery() { + const agent = useAgent() + return useQuery<ServiceConfig>({ + refetchOnWindowFocus: true, + staleTime: STALE.MINUTES.FIVE, + queryKey: ['service-config'], + queryFn: async () => { + try { + const {data} = await agent.api.app.bsky.unspecced.getConfig() + return { + checkEmailConfirmed: Boolean(data.checkEmailConfirmed), + topicsEnabled: Boolean(data.topicsEnabled), + } + } catch (e) { + return { + checkEmailConfirmed: false, + topicsEnabled: false, + } + } + }, + }) +} diff --git a/src/state/queries/trending/useTrendingTopics.ts b/src/state/queries/trending/useTrendingTopics.ts new file mode 100644 index 000000000..310f64e9f --- /dev/null +++ b/src/state/queries/trending/useTrendingTopics.ts @@ -0,0 +1,49 @@ +import React from 'react' +import {AppBskyUnspeccedDefs} from '@atproto/api' +import {hasMutedWord} from '@atproto/api/dist/moderation/mutewords' +import {useQuery} from '@tanstack/react-query' + +import {STALE} from '#/state/queries' +import {usePreferencesQuery} from '#/state/queries/preferences' +import {useAgent} from '#/state/session' + +export type TrendingTopic = AppBskyUnspeccedDefs.TrendingTopic + +export const DEFAULT_LIMIT = 14 + +export const trendingTopicsQueryKey = ['trending-topics'] + +export function useTrendingTopics() { + const agent = useAgent() + const {data: preferences} = usePreferencesQuery() + const mutedWords = React.useMemo(() => { + return preferences?.moderationPrefs?.mutedWords || [] + }, [preferences?.moderationPrefs]) + + return useQuery({ + refetchOnWindowFocus: true, + staleTime: STALE.MINUTES.THIRTY, + queryKey: trendingTopicsQueryKey, + async queryFn() { + const {data} = await agent.api.app.bsky.unspecced.getTrendingTopics({ + limit: DEFAULT_LIMIT, + }) + + const {topics, suggested} = data + return { + topics: topics.filter(t => { + return !hasMutedWord({ + mutedWords, + text: t.topic + ' ' + t.displayName + ' ' + t.description, + }) + }), + suggested: suggested.filter(t => { + return !hasMutedWord({ + mutedWords, + text: t.topic + ' ' + t.displayName + ' ' + t.description, + }) + }), + } + }, + }) +} diff --git a/src/state/trending-config.tsx b/src/state/trending-config.tsx new file mode 100644 index 000000000..a7694993f --- /dev/null +++ b/src/state/trending-config.tsx @@ -0,0 +1,70 @@ +import React from 'react' + +import {useGate} from '#/lib/statsig/statsig' +import {useLanguagePrefs} from '#/state/preferences/languages' +import {useServiceConfigQuery} from '#/state/queries/service-config' +import {device} from '#/storage' + +type Context = { + enabled: boolean +} + +const Context = React.createContext<Context>({ + enabled: false, +}) + +export function Provider({children}: React.PropsWithChildren<{}>) { + const gate = useGate() + const langPrefs = useLanguagePrefs() + const {data: config, isLoading: isInitialLoad} = useServiceConfigQuery() + const ctx = React.useMemo<Context>(() => { + if (__DEV__) { + return {enabled: true} + } + + /* + * Only English during beta period + */ + if ( + !!langPrefs.contentLanguages.length && + !langPrefs.contentLanguages.includes('en') + ) { + return {enabled: false} + } + + /* + * While loading, use cached value + */ + const cachedEnabled = device.get(['trendingBetaEnabled']) + if (isInitialLoad) { + return {enabled: Boolean(cachedEnabled)} + } + + /* + * Doing an extra check here to reduce hits to statsig. If it's disabled on + * the server, we can exit early. + */ + const enabled = Boolean(config?.topicsEnabled) + if (!enabled) { + // cache for next reload + device.set(['trendingBetaEnabled'], enabled) + return {enabled: false} + } + + /* + * Service is enabled, but also check statsig in case we're rolling back. + */ + const gateEnabled = gate('trending_topics_beta') + const _enabled = enabled && gateEnabled + + // update cache + device.set(['trendingBetaEnabled'], _enabled) + + return {enabled: _enabled} + }, [isInitialLoad, config, gate, langPrefs.contentLanguages]) + return <Context.Provider value={ctx}>{children}</Context.Provider> +} + +export function useTrendingConfig() { + return React.useContext(Context) +} |