Content prefs screen
This commit is contained in:
@@ -82,6 +82,7 @@ import {ProfileLabelerLikedByScreen} from '#/screens/Profile/ProfileLabelerLiked
|
|||||||
import {SearchScreen} from '#/screens/Search'
|
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 {ContentPreferences} from '#/screens/Settings/ContentPreferences'
|
||||||
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings'
|
||||||
import {
|
import {
|
||||||
StarterPackScreen,
|
StarterPackScreen,
|
||||||
@@ -375,6 +376,14 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
|||||||
requireAuth: true,
|
requireAuth: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="ContentPreferences"
|
||||||
|
getComponent={() => ContentPreferences}
|
||||||
|
options={{
|
||||||
|
title: title(msg`Content Preferences`),
|
||||||
|
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
|
||||||
|
ContentPreferences: 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',
|
||||||
|
ContentPreferences: '/settings/content-preferences',
|
||||||
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/content-preferences"
|
||||||
|
label={_(msg`Content preferences`)}>
|
||||||
|
<SettingsList.ItemIcon icon={CircleInfo} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Content preferences</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
</SettingsList.LinkItem>
|
||||||
<SettingsList.Divider />
|
<SettingsList.Divider />
|
||||||
{isNative && (
|
{isNative && (
|
||||||
<Toggle.Item
|
<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