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/account", server.WebGeneric)
|
||||||
e.GET("/settings/privacy-and-security", server.WebGeneric)
|
e.GET("/settings/privacy-and-security", server.WebGeneric)
|
||||||
e.GET("/settings/content-and-media", 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/about", server.WebGeneric)
|
||||||
e.GET("/settings/app-icon", server.WebGeneric)
|
e.GET("/settings/app-icon", server.WebGeneric)
|
||||||
e.GET("/sys/debug", server.WebGeneric)
|
e.GET("/sys/debug", server.WebGeneric)
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ import {SearchScreen} from '#/screens/Search'
|
|||||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||||
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
|
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
|
||||||
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
||||||
|
import {SettingsInterests} from '#/screens/Settings/SettingsInterests'
|
||||||
import {
|
import {
|
||||||
StarterPackScreen,
|
StarterPackScreen,
|
||||||
StarterPackScreenShort,
|
StarterPackScreenShort,
|
||||||
@@ -375,6 +376,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="SettingsInterests"
|
||||||
|
getComponent={() => SettingsInterests}
|
||||||
|
options={{
|
||||||
|
title: title(msg`Your interests`),
|
||||||
|
requireAuth: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="AboutSettings"
|
name="AboutSettings"
|
||||||
getComponent={() => AboutSettingsScreen}
|
getComponent={() => AboutSettingsScreen}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {NavigationState, PartialState} from '@react-navigation/native'
|
import {type NavigationState, type PartialState} from '@react-navigation/native'
|
||||||
import type {NativeStackNavigationProp} from '@react-navigation/native-stack'
|
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'
|
export type {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@ export type CommonNavigatorParams = {
|
|||||||
AccountSettings: undefined
|
AccountSettings: undefined
|
||||||
PrivacyAndSecuritySettings: undefined
|
PrivacyAndSecuritySettings: undefined
|
||||||
ContentAndMediaSettings: undefined
|
ContentAndMediaSettings: undefined
|
||||||
|
SettingsInterests: undefined
|
||||||
AboutSettings: undefined
|
AboutSettings: undefined
|
||||||
AppIconSettings: undefined
|
AppIconSettings: undefined
|
||||||
Search: {q?: string}
|
Search: {q?: string}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export const router = new Router({
|
|||||||
AccountSettings: '/settings/account',
|
AccountSettings: '/settings/account',
|
||||||
PrivacyAndSecuritySettings: '/settings/privacy-and-security',
|
PrivacyAndSecuritySettings: '/settings/privacy-and-security',
|
||||||
ContentAndMediaSettings: '/settings/content-and-media',
|
ContentAndMediaSettings: '/settings/content-and-media',
|
||||||
|
SettingsInterests: '/settings/interests',
|
||||||
AboutSettings: '/settings/about',
|
AboutSettings: '/settings/about',
|
||||||
AppIconSettings: '/settings/app-icon',
|
AppIconSettings: '/settings/app-icon',
|
||||||
// support
|
// support
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {useTrendingConfig} from '#/state/trending-config'
|
|||||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
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 {Hashtag_Stroke2_Corner0_Rounded as HashtagIcon} from '#/components/icons/Hashtag'
|
||||||
import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
|
import {Home_Stroke2_Corner2_Rounded as HomeIcon} from '#/components/icons/Home'
|
||||||
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
|
import {Macintosh_Stroke2_Corner2_Rounded as MacintoshIcon} from '#/components/icons/Macintosh'
|
||||||
@@ -86,6 +87,14 @@ export function ContentAndMediaSettingsScreen({}: Props) {
|
|||||||
<Trans>External media</Trans>
|
<Trans>External media</Trans>
|
||||||
</SettingsList.ItemText>
|
</SettingsList.ItemText>
|
||||||
</SettingsList.LinkItem>
|
</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 />
|
<SettingsList.Divider />
|
||||||
{isNative && (
|
{isNative && (
|
||||||
<Toggle.Item
|
<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