diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 73fa710195..b74df5ff10 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -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) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 420a49d4c6..d89d45919c 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -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, }} /> + SettingsInterests} + options={{ + title: title(msg`Your interests`), + requireAuth: true, + }} + /> AboutSettingsScreen} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 0e38c92627..658b68db85 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -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} diff --git a/src/routes.ts b/src/routes.ts index b6a11acbf7..68c39e7fca 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -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 diff --git a/src/screens/Settings/ContentAndMediaSettings.tsx b/src/screens/Settings/ContentAndMediaSettings.tsx index 57b86fb2bf..6fa90b1e2a 100644 --- a/src/screens/Settings/ContentAndMediaSettings.tsx +++ b/src/screens/Settings/ContentAndMediaSettings.tsx @@ -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) { External media + + + + Your interests + + {isNative && ( + + + + + Your interests + + + {isSaving && } + + + + + + Selecting interests from the list below helps us deliver you + higher quality content. + + + + + + {preferences ? ( + + ) : ( + + + + )} + + + + ) +} + +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(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 ( + + + {INTERESTS.map(interest => { + const name = interestsDisplayNames[interest] + if (!name) return null + return ( + + + + ) + })} + + + ) +} + +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 ( + + + {interestsDisplayNames[interest]} + + + ) +} + +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', +]