Content interests (#8114)
* Content prefs screen * Handle debounce, rename * Fix format * Let's just use interests * Reuse hook
This commit is contained in:
@@ -275,6 +275,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/settings/account", server.WebGeneric)
|
||||
e.GET("/settings/privacy-and-security", server.WebGeneric)
|
||||
e.GET("/settings/content-and-media", server.WebGeneric)
|
||||
e.GET("/settings/interests", server.WebGeneric)
|
||||
e.GET("/settings/about", server.WebGeneric)
|
||||
e.GET("/settings/app-icon", server.WebGeneric)
|
||||
e.GET("/sys/debug", server.WebGeneric)
|
||||
|
||||
@@ -83,6 +83,7 @@ import {SearchScreen} from '#/screens/Search'
|
||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
|
||||
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
||||
import {SettingsInterests} from '#/screens/Settings/SettingsInterests'
|
||||
import {
|
||||
StarterPackScreen,
|
||||
StarterPackScreenShort,
|
||||
@@ -375,6 +376,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="SettingsInterests"
|
||||
getComponent={() => SettingsInterests}
|
||||
options={{
|
||||
title: title(msg`Your interests`),
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AboutSettings"
|
||||
getComponent={() => AboutSettingsScreen}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {NavigationState, PartialState} from '@react-navigation/native'
|
||||
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||
import {type NavigationState, type PartialState} from '@react-navigation/native'
|
||||
import {type NativeStackNavigationProp} from '@react-navigation/native-stack'
|
||||
|
||||
import {VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||
import {type VideoFeedSourceContext} from '#/screens/VideoFeed/types'
|
||||
|
||||
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||
|
||||
@@ -51,6 +51,7 @@ export type CommonNavigatorParams = {
|
||||
AccountSettings: undefined
|
||||
PrivacyAndSecuritySettings: undefined
|
||||
ContentAndMediaSettings: undefined
|
||||
SettingsInterests: undefined
|
||||
AboutSettings: undefined
|
||||
AppIconSettings: undefined
|
||||
Search: {q?: string}
|
||||
|
||||
@@ -45,6 +45,7 @@ export const router = new Router({
|
||||
AccountSettings: '/settings/account',
|
||||
PrivacyAndSecuritySettings: '/settings/privacy-and-security',
|
||||
ContentAndMediaSettings: '/settings/content-and-media',
|
||||
SettingsInterests: '/settings/interests',
|
||||
AboutSettings: '/settings/about',
|
||||
AppIconSettings: '/settings/app-icon',
|
||||
// support
|
||||
|
||||
@@ -18,6 +18,7 @@ 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'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag'
|
||||
import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
|
||||
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
|
||||
@@ -86,6 +87,14 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
||||
<Trans>External media</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/interests"
|
||||
label={_(msg`Your interests`)}>
|
||||
<SettingsList.ItemIcon icon={CircleInfo} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Your interests</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.Divider />
|
||||
{isNative && (
|
||||
<Toggle.Item
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
import {useMemo, useState} from 'react'
|
||||
import {type TextStyle, View, type ViewStyle} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import {
|
||||
preferencesQueryKey,
|
||||
usePreferencesQuery,
|
||||
} from '#/state/queries/preferences'
|
||||
import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types'
|
||||
import {useAgent} from '#/state/session'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
|
||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import * as Toggle from '#/components/forms/Toggle'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function SettingsInterests() {
|
||||
const t = useTheme()
|
||||
const gutters = useGutters(['base'])
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Your interests</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot>{isSaving && <Loader />}</Layout.Header.Slot>
|
||||
</Layout.Header.Outer>
|
||||
<Layout.Content>
|
||||
<View style={[gutters, a.gap_lg]}>
|
||||
<Text
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>
|
||||
Selecting interests from the list below helps us deliver you
|
||||
higher quality content.
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
{preferences ? (
|
||||
<Inner preferences={preferences} setIsSaving={setIsSaving} />
|
||||
) : (
|
||||
<View style={[a.flex_row, a.justify_center, a.p_lg]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function Inner({
|
||||
preferences,
|
||||
setIsSaving,
|
||||
}: {
|
||||
preferences: UsePreferencesQueryResponse
|
||||
setIsSaving: (isSaving: boolean) => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const qc = useQueryClient()
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
const preselectedInterests = useMemo(
|
||||
() => preferences.interests.tags || [],
|
||||
[preferences.interests.tags],
|
||||
)
|
||||
const [interests, setInterests] = useState<string[]>(preselectedInterests)
|
||||
|
||||
const saveInterests = useMemo(() => {
|
||||
return debounce(async (interests: string[]) => {
|
||||
const noEdits =
|
||||
interests.length === preselectedInterests.length &&
|
||||
preselectedInterests.every(pre => {
|
||||
return interests.find(int => int === pre)
|
||||
})
|
||||
|
||||
if (noEdits) return
|
||||
|
||||
setIsSaving(true)
|
||||
|
||||
try {
|
||||
await agent.setInterestsPref({tags: interests})
|
||||
await qc.invalidateQueries({queryKey: preferencesQueryKey})
|
||||
Toast.show(
|
||||
_(msg({message: 'Content preferences updated!', context: 'toast'})),
|
||||
)
|
||||
} catch (error) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg({
|
||||
message: 'Failed to save content prefefences.',
|
||||
context: 'toast',
|
||||
}),
|
||||
),
|
||||
'xmark',
|
||||
)
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}, 1500)
|
||||
}, [_, agent, setIsSaving, qc, preselectedInterests])
|
||||
|
||||
const onChangeInterests = async (interests: string[]) => {
|
||||
setInterests(interests)
|
||||
saveInterests(interests)
|
||||
}
|
||||
|
||||
return (
|
||||
<Toggle.Group
|
||||
values={interests}
|
||||
onChange={onChangeInterests}
|
||||
label={_(msg`Select your interests from the options below`)}>
|
||||
<View style={[a.flex_row, a.flex_wrap, a.gap_sm]}>
|
||||
{INTERESTS.map(interest => {
|
||||
const name = interestsDisplayNames[interest]
|
||||
if (!name) return null
|
||||
return (
|
||||
<Toggle.Item
|
||||
key={interest}
|
||||
name={interest}
|
||||
label={interestsDisplayNames[interest]}>
|
||||
<InterestButton interest={interest} />
|
||||
</Toggle.Item>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</Toggle.Group>
|
||||
)
|
||||
}
|
||||
|
||||
export function InterestButton({interest}: {interest: string}) {
|
||||
const t = useTheme()
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
const ctx = Toggle.useItemContext()
|
||||
|
||||
const styles = useMemo(() => {
|
||||
const hovered: ViewStyle[] = [t.atoms.bg_contrast_100]
|
||||
const focused: ViewStyle[] = []
|
||||
const pressed: ViewStyle[] = []
|
||||
const selected: ViewStyle[] = [t.atoms.bg_contrast_900]
|
||||
const selectedHover: ViewStyle[] = [t.atoms.bg_contrast_975]
|
||||
const textSelected: TextStyle[] = [t.atoms.text_inverted]
|
||||
|
||||
return {
|
||||
hovered,
|
||||
focused,
|
||||
pressed,
|
||||
selected,
|
||||
selectedHover,
|
||||
textSelected,
|
||||
}
|
||||
}, [t])
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.rounded_full,
|
||||
a.py_md,
|
||||
a.px_xl,
|
||||
t.atoms.bg_contrast_50,
|
||||
ctx.hovered ? styles.hovered : {},
|
||||
ctx.focused ? styles.hovered : {},
|
||||
ctx.pressed ? styles.hovered : {},
|
||||
ctx.selected ? styles.selected : {},
|
||||
ctx.selected && (ctx.hovered || ctx.focused || ctx.pressed)
|
||||
? styles.selectedHover
|
||||
: {},
|
||||
]}>
|
||||
<Text
|
||||
selectable={false}
|
||||
style={[
|
||||
{
|
||||
color: t.palette.contrast_900,
|
||||
},
|
||||
a.font_bold,
|
||||
ctx.selected ? styles.textSelected : {},
|
||||
]}>
|
||||
{interestsDisplayNames[interest]}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const INTERESTS = [
|
||||
'animals',
|
||||
'art',
|
||||
'books',
|
||||
'comedy',
|
||||
'comics',
|
||||
'culture',
|
||||
'dev',
|
||||
'education',
|
||||
'food',
|
||||
'gaming',
|
||||
'journalism',
|
||||
'movies',
|
||||
'music',
|
||||
'nature',
|
||||
'news',
|
||||
'pets',
|
||||
'photography',
|
||||
'politics',
|
||||
'science',
|
||||
'sports',
|
||||
'tech',
|
||||
'tv',
|
||||
'writers',
|
||||
]
|
||||
Reference in New Issue
Block a user