personal fork of bluesky app bsky.kelinci.net
0

Configure Feed

Select the types of activity you want to include in your feed.

social-app-fork / src / screens / Settings / InterestsSettings.tsx
5.5 kB 164 lines
1import { useMemo, useState } from 'react'; 2 3import { Checkbox } from '@base-ui/react/checkbox'; 4import { CheckboxGroup } from '@base-ui/react/checkbox-group'; 5import { useQueryClient } from '@tanstack/react-query'; 6 7import { useDebouncedCallback } from '#/lib/hooks/use-debounced-callback'; 8import { useTitle } from '#/lib/hooks/useTitle'; 9import { interests as allInterests, useInterestsDisplayNames } from '#/lib/interests'; 10 11import { preferencesQueryKey, usePreferencesQuery } from '#/state/queries/preferences'; 12import { setInterestsPref } from '#/state/queries/preferences/agent'; 13import type { UsePreferencesQueryResponse } from '#/state/queries/preferences/types'; 14import { createGetSuggestedFeedsQueryKey } from '#/state/queries/trending/useGetSuggestedFeedsQuery'; 15import { createGetSuggestedUsersForDiscoverQueryKey } from '#/state/queries/trending/useGetSuggestedUsersForDiscoverQuery'; 16import { createGetSuggestedUsersForExploreQueryKey } from '#/state/queries/trending/useGetSuggestedUsersForExploreQuery'; 17import { createGetSuggestedUsersForSeeMoreQueryKey } from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery'; 18import { createSuggestedStarterPacksQueryKey } from '#/state/queries/useSuggestedStarterPacksQuery'; 19import { getClients } from '#/state/session'; 20 21import { Spinner } from '#/components/Spinner'; 22import { Text } from '#/components/Text'; 23import * as Toast from '#/components/Toast'; 24import { Admonition } from '#/components/web/Admonition'; 25import * as Layout from '#/components/web/Layout'; 26 27import { m } from '#/paraglide/messages'; 28 29import * as styles from './InterestsSettings.css'; 30 31export function InterestsSettingsScreen() { 32 useTitle(m['common.interest.yourInterests']()); 33 const { data: preferences } = usePreferencesQuery(); 34 const [isSaving, setIsSaving] = useState(false); 35 36 return ( 37 <Layout.Screen> 38 <Layout.Header.Outer> 39 <Layout.Header.BackButton /> 40 <Layout.Header.Content> 41 <Layout.Header.TitleText>{m['common.interest.yourInterests']()}</Layout.Header.TitleText> 42 </Layout.Header.Content> 43 {isSaving && ( 44 <Layout.Header.Slot> 45 <Spinner color="default" label={m['common.status.saving']()} size="sm" /> 46 </Layout.Header.Slot> 47 )} 48 </Layout.Header.Outer> 49 <Layout.Content> 50 <div className={styles.body}> 51 <Text color="textContrastMedium" size="md_sub"> 52 {m['screens.settings.interests.helpHint']()} 53 </Text> 54 55 {preferences ? ( 56 <Inner preferences={preferences} setIsSaving={setIsSaving} /> 57 ) : ( 58 <div className={styles.loaderWrap}> 59 <Spinner color="default" label={m['common.status.loading']()} size="2xl" /> 60 </div> 61 )} 62 </div> 63 </Layout.Content> 64 </Layout.Screen> 65 ); 66} 67 68function Inner({ 69 preferences, 70 setIsSaving, 71}: { 72 preferences: UsePreferencesQueryResponse; 73 setIsSaving: (isSaving: boolean) => void; 74}) { 75 const { pds } = getClients(); 76 const qc = useQueryClient(); 77 const interestsDisplayNames = useInterestsDisplayNames(); 78 const preselectedInterests = useMemo(() => preferences.interests.tags || [], [preferences.interests.tags]); 79 const [interests, setInterests] = useState<string[]>(preselectedInterests); 80 81 // persist the edit even if the user leaves before the window closes 82 const saveInterests = useDebouncedCallback( 83 async (nextInterests: string[]) => { 84 const noEdits = 85 nextInterests.length === preselectedInterests.length && 86 preselectedInterests.every((pre) => { 87 return nextInterests.find((int) => int === pre); 88 }); 89 90 if (noEdits) { 91 return; 92 } 93 94 setIsSaving(true); 95 96 try { 97 await setInterestsPref(pds!, { tags: nextInterests }); 98 qc.setQueriesData({ queryKey: preferencesQueryKey }, (old?: UsePreferencesQueryResponse) => { 99 if (!old) { 100 return old; 101 } 102 old.interests.tags = nextInterests; 103 return old; 104 }); 105 await Promise.all([ 106 qc.resetQueries({ queryKey: createSuggestedStarterPacksQueryKey() }), 107 qc.resetQueries({ queryKey: createGetSuggestedFeedsQueryKey() }), 108 qc.resetQueries({ 109 queryKey: createGetSuggestedUsersForDiscoverQueryKey({}), 110 }), 111 qc.resetQueries({ 112 queryKey: createGetSuggestedUsersForExploreQueryKey({}), 113 }), 114 qc.resetQueries({ 115 queryKey: createGetSuggestedUsersForSeeMoreQueryKey({}), 116 }), 117 ]); 118 119 Toast.show(m['screens.settings.interests.updatedToast']()); 120 } catch { 121 Toast.show(m['screens.settings.interests.saveError'](), { 122 type: 'error', 123 }); 124 } finally { 125 setIsSaving(false); 126 } 127 }, 128 1500, 129 { onUnmount: 'flush' }, 130 ); 131 132 const onChangeInterests = (nextInterests: string[]) => { 133 setInterests(nextInterests); 134 saveInterests(nextInterests); 135 }; 136 137 return ( 138 <> 139 {interests.length === 0 && ( 140 <Admonition type="tip">{m['screens.settings.interests.recommendTwo']()}</Admonition> 141 )} 142 <CheckboxGroup 143 aria-label={m['screens.settings.interests.selectPrompt']()} 144 className={styles.chipWrap} 145 onValueChange={(value) => onChangeInterests(value)} 146 value={interests} 147 > 148 {allInterests.map((interest) => { 149 const name = interestsDisplayNames[interest]; 150 if (!name) { 151 return null; 152 } 153 return ( 154 <Checkbox.Root aria-label={name} className={styles.chip} key={interest} name={interest}> 155 <Text className={styles.chipText} selectable={false} size="md_sub" weight="semiBold"> 156 {name} 157 </Text> 158 </Checkbox.Root> 159 ); 160 })} 161 </CheckboxGroup> 162 </> 163 ); 164}