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 1358ad47fd.

* 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>
This commit is contained in:
Eric Bailey
2024-12-17 21:45:39 -06:00
committed by GitHub
parent a07949ec8e
commit a2019aceec
29 changed files with 1268 additions and 80 deletions
+2
View File
@@ -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 {
+4 -1
View File
@@ -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>
+69
View File
@@ -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)
}
+1
View File
@@ -6,6 +6,7 @@ export const STALE = {
MINUTES: {
ONE: 1e3 * 60,
FIVE: 1e3 * 60 * 5,
THIRTY: 1e3 * 60 * 30,
},
HOURS: {
ONE: 1e3 * 60 * 60,
+32
View File
@@ -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,
}
}
},
})
}
@@ -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,
})
}),
}
},
})
}
+70
View File
@@ -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)
}