Content prefs screen
This commit is contained in:
@@ -82,6 +82,7 @@ import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLiked
|
||||
import {SearchScreen} from '#/screens/Search'
|
||||
import {AppearanceSettingsScreen} from '#/screens/Settings/AppearanceSettings'
|
||||
import {AppIconSettingsScreen} from '#/screens/Settings/AppIconSettings'
|
||||
import {ContentPreferences} from '#/screens/Settings/ContentPreferences'
|
||||
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
||||
import {
|
||||
StarterPackScreen,
|
||||
@@ -375,6 +376,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
requireAuth: true,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ContentPreferences"
|
||||
getComponent={() => ContentPreferences}
|
||||
options={{
|
||||
title: title(msg`Content Preferences`),
|
||||
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
|
||||
ContentPreferences: 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',
|
||||
ContentPreferences: '/settings/content-preferences',
|
||||
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/content-preferences"
|
||||
label={_(msg`Content preferences`)}>
|
||||
<SettingsList.ItemIcon icon={CircleInfo} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Content preferences</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
<SettingsList.Divider />
|
||||
{isNative && (
|
||||
<Toggle.Item
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
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 deepEqual from 'lodash.isequal'
|
||||
|
||||
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 {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 ContentPreferences() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const qc = useQueryClient()
|
||||
const gutters = useGutters(['base'])
|
||||
const {data: preferences} = usePreferencesQuery()
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const saveInterests = useMemo(() => {
|
||||
return debounce(async (interests: string[]) => {
|
||||
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)
|
||||
}
|
||||
}, 1000)
|
||||
}, [_, agent, setIsSaving, qc])
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Content preferences</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} saveInterests={saveInterests} />
|
||||
) : (
|
||||
<View style={[a.flex_row, a.justify_center, a.p_lg]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Layout.Content>
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function Inner({
|
||||
preferences,
|
||||
saveInterests,
|
||||
}: {
|
||||
preferences: UsePreferencesQueryResponse
|
||||
saveInterests: (interests: string[]) => Promise<void>
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const interestsDisplayNames = useInterestsDisplayNames()
|
||||
const preselectedInterests = preferences.interests.tags || []
|
||||
const [interests, setInterests] = useState<string[]>(preselectedInterests)
|
||||
|
||||
const onChangeInterests = async (interests: string[]) => {
|
||||
setInterests(interests)
|
||||
const isEdited = !deepEqual(interests, preselectedInterests)
|
||||
console.log(isEdited, {interests, preselectedInterests})
|
||||
if (isEdited) 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',
|
||||
]
|
||||
|
||||
function useInterestsDisplayNames() {
|
||||
const {_} = useLingui()
|
||||
|
||||
return useMemo<Record<string, string>>(() => {
|
||||
return {
|
||||
// Keep this alphabetized
|
||||
animals: _(msg`Animals`),
|
||||
art: _(msg`Art`),
|
||||
books: _(msg`Books`),
|
||||
comedy: _(msg`Comedy`),
|
||||
comics: _(msg`Comics`),
|
||||
culture: _(msg`Culture`),
|
||||
dev: _(msg`Software Dev`),
|
||||
education: _(msg`Education`),
|
||||
food: _(msg`Food`),
|
||||
gaming: _(msg`Video Games`),
|
||||
journalism: _(msg`Journalism`),
|
||||
movies: _(msg`Movies`),
|
||||
music: _(msg`Music`),
|
||||
nature: _(msg`Nature`),
|
||||
news: _(msg`News`),
|
||||
pets: _(msg`Pets`),
|
||||
photography: _(msg`Photography`),
|
||||
politics: _(msg`Politics`),
|
||||
science: _(msg`Science`),
|
||||
sports: _(msg`Sports`),
|
||||
tech: _(msg`Tech`),
|
||||
tv: _(msg`TV`),
|
||||
writers: _(msg`Writers`),
|
||||
}
|
||||
}, [_])
|
||||
}
|
||||
Reference in New Issue
Block a user