Merge remote-tracking branch 'origin/main' into explore/refetch

This commit is contained in:
Hailey
2025-04-04 17:01:00 -07:00
15 changed files with 334 additions and 155 deletions
+1 -1
View File
@@ -58,7 +58,7 @@
"icons:optimize": "svgo -f ./assets/icons" "icons:optimize": "svgo -f ./assets/icons"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.14.19", "@atproto/api": "^0.14.20",
"@bitdrift/react-native": "^0.6.8", "@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
+3 -3
View File
@@ -19,7 +19,7 @@ export function AvatarStack({
numPending?: number numPending?: number
backgroundColor?: string backgroundColor?: string
}) { }) {
const halfSize = size / 2 const translation = size / 3 // overlap by 1/3
const t = useTheme() const t = useTheme()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
@@ -43,7 +43,7 @@ export function AvatarStack({
a.flex_row, a.flex_row,
a.align_center, a.align_center,
a.relative, a.relative,
{width: size + (items.length - 1) * halfSize}, {width: size + (items.length - 1) * (size - translation)},
]}> ]}>
{items.map((item, i) => ( {items.map((item, i) => (
<View <View
@@ -54,7 +54,7 @@ export function AvatarStack({
{ {
width: size, width: size,
height: size, height: size,
left: i * -halfSize, left: i * -translation,
borderWidth: 1, borderWidth: 1,
borderColor: backgroundColor ?? t.atoms.bg.backgroundColor, borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
borderRadius: 999, borderRadius: 999,
+4 -4
View File
@@ -152,7 +152,7 @@ export function AvatarPlaceholder() {
<View <View
style={[ style={[
a.rounded_full, a.rounded_full,
t.atoms.bg_contrast_50, t.atoms.bg_contrast_25,
{ {
width: 40, width: 40,
height: 40, height: 40,
@@ -203,7 +203,7 @@ export function NameAndHandlePlaceholder() {
<View <View
style={[ style={[
a.rounded_xs, a.rounded_xs,
t.atoms.bg_contrast_50, t.atoms.bg_contrast_25,
{ {
width: '60%', width: '60%',
height: 14, height: 14,
@@ -214,7 +214,7 @@ export function NameAndHandlePlaceholder() {
<View <View
style={[ style={[
a.rounded_xs, a.rounded_xs,
t.atoms.bg_contrast_50, t.atoms.bg_contrast_25,
{ {
width: '40%', width: '40%',
height: 10, height: 10,
@@ -275,7 +275,7 @@ export function DescriptionPlaceholder({
style={[ style={[
a.rounded_xs, a.rounded_xs,
a.w_full, a.w_full,
t.atoms.bg_contrast_50, t.atoms.bg_contrast_25,
{height: 12, width: i + 1 === numberOfLines ? '60%' : '100%'}, {height: 12, width: i + 1 === numberOfLines ? '60%' : '100%'},
]} ]}
/> />
+11 -2
View File
@@ -1,5 +1,12 @@
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react' import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {ScrollView, TextInput, useWindowDimensions, View} from 'react-native' import {
ScrollView,
type StyleProp,
TextInput,
useWindowDimensions,
View,
type ViewStyle,
} from 'react-native'
import Animated, { import Animated, {
LayoutAnimationConfig, LayoutAnimationConfig,
LinearTransition, LinearTransition,
@@ -453,6 +460,7 @@ let Tabs = ({
hasSearchText, hasSearchText,
interestsDisplayNames, interestsDisplayNames,
TabComponent = Tab, TabComponent = Tab,
contentContainerStyle,
}: { }: {
onSelectTab: (tab: string) => void onSelectTab: (tab: string) => void
interests: string[] interests: string[]
@@ -460,6 +468,7 @@ let Tabs = ({
hasSearchText: boolean hasSearchText: boolean
interestsDisplayNames: Record<string, string> interestsDisplayNames: Record<string, string>
TabComponent?: React.ComponentType<React.ComponentProps<typeof Tab>> TabComponent?: React.ComponentType<React.ComponentProps<typeof Tab>>
contentContainerStyle?: StyleProp<ViewStyle>
}): React.ReactNode => { }): React.ReactNode => {
const listRef = useRef<ScrollView>(null) const listRef = useRef<ScrollView>(null)
const [scrollX, setScrollX] = useState(0) const [scrollX, setScrollX] = useState(0)
@@ -520,7 +529,7 @@ let Tabs = ({
<ScrollView <ScrollView
ref={listRef} ref={listRef}
horizontal horizontal
contentContainerStyle={[a.gap_sm, a.px_lg]} contentContainerStyle={[a.gap_sm, a.px_lg, contentContainerStyle]}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
decelerationRate="fast" decelerationRate="fast"
snapToOffsets={ snapToOffsets={
+57 -101
View File
@@ -15,15 +15,15 @@ import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger' import {logger} from '#/logger'
import {type MetricEvents} from '#/logger/metrics' import {type MetricEvents} from '#/logger/metrics'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useActorSearchPaginated} from '#/state/queries/actor-search'
import { import {
type FeedPreviewItem, type FeedPreviewItem,
useFeedPreviews, useFeedPreviews,
} from '#/state/queries/explore-feed-previews' } from '#/state/queries/explore-feed-previews'
import {useGetPopularFeedsQuery} from '#/state/queries/feed' import {useGetPopularFeedsQuery} from '#/state/queries/feed'
import {Nux, useNux} from '#/state/queries/nuxs'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery' import {useGetSuggestedFeedsQuery} from '#/state/queries/trending/useGetSuggestedFeedsQuery'
import {useGetSuggestedUsersQuery} from '#/state/queries/trending/useGetSuggestedUsersQuery'
import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery' import {useSuggestedStarterPacksQuery} from '#/state/queries/useSuggestedStarterPacksQuery'
import {useProgressGuide} from '#/state/shell/progress-guide' import {useProgressGuide} from '#/state/shell/progress-guide'
import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed' import {isThreadChildAt, isThreadParentAt} from '#/view/com/posts/PostFeed'
@@ -36,6 +36,7 @@ import {
StarterPackCard, StarterPackCard,
StarterPackCardSkeleton, StarterPackCardSkeleton,
} from '#/screens/Search/components/StarterPackCard' } from '#/screens/Search/components/StarterPackCard'
import {ExploreInterestsCard} from '#/screens/Search/modules/ExploreInterestsCard'
import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations' import {ExploreRecommendations} from '#/screens/Search/modules/ExploreRecommendations'
import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics' import {ExploreTrendingTopics} from '#/screens/Search/modules/ExploreTrendingTopics'
import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos' import {ExploreTrendingVideos} from '#/screens/Search/modules/ExploreTrendingVideos'
@@ -44,6 +45,7 @@ import {Button} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard' import * as FeedCard from '#/components/FeedCard'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron' import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {type Props as IcoProps} from '#/components/icons/common'
import {type Props as SVGIconProps} from '#/components/icons/common' import {type Props as SVGIconProps} from '#/components/icons/common'
import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle' import {ListSparkle_Stroke2_Corner0_Rounded as ListSparkle} from '#/components/icons/ListSparkle'
import {StarterPack} from '#/components/icons/StarterPack' import {StarterPack} from '#/components/icons/StarterPack'
@@ -55,7 +57,6 @@ import * as ModuleHeader from './components/ModuleHeader'
import { import {
SuggestedAccountsTabBar, SuggestedAccountsTabBar,
SuggestedProfileCard, SuggestedProfileCard,
useLoadEnoughProfiles,
} from './modules/ExploreSuggestedAccounts' } from './modules/ExploreSuggestedAccounts'
function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) { function LoadMore({item}: {item: ExploreScreenItems & {type: 'loadMore'}}) {
@@ -110,6 +111,7 @@ type ExploreScreenItems =
key: string key: string
title: string title: string
icon: React.ComponentType<SVGIconProps> icon: React.ComponentType<SVGIconProps>
iconSize?: IcoProps['size']
searchButton?: { searchButton?: {
label: string label: string
metricsTag: MetricEvents['explore:module:searchButtonPress']['module'] metricsTag: MetricEvents['explore:module:searchButtonPress']['module']
@@ -142,7 +144,7 @@ type ExploreScreenItems =
| { | {
type: 'profile' type: 'profile'
key: string key: string
profile: AppBskyActorDefs.ProfileView profile: AppBskyActorDefs.ProfileViewBasic
recId?: number recId?: number
} }
| { | {
@@ -181,6 +183,10 @@ type ExploreScreenItems =
key: string key: string
} }
| FeedPreviewItem | FeedPreviewItem
| {
type: 'interests-card'
key: 'interests-card'
}
export function Explore({ export function Explore({
focusSearchInput, focusSearchInput,
@@ -197,33 +203,13 @@ export function Explore({
const gate = useGate() const gate = useGate()
const guide = useProgressGuide('follow-10') const guide = useProgressGuide('follow-10')
const [selectedInterest, setSelectedInterest] = useState<string | null>(null) const [selectedInterest, setSelectedInterest] = useState<string | null>(null)
// TODO always get at least 10 back
const { const {
data: suggestedProfiles, data: suggestedUsers,
hasNextPage: hasNextSuggestedProfilesPage, isLoading: suggestedUsersIsLoading,
isLoading: isLoadingSuggestedProfiles, error: suggestedUsersError,
isFetchingNextPage: isFetchingNextSuggestedProfilesPage, } = useGetSuggestedUsersQuery({
error: suggestedProfilesError, category: selectedInterest,
fetchNextPage: fetchNextSuggestedProfilesPage,
} = useSuggestedFollowsQuery({limit: 3, subsequentPageLimit: 10})
const {
data: interestProfiles,
hasNextPage: hasNextInterestProfilesPage,
isLoading: isLoadingInterestProfiles,
isFetchingNextPage: isFetchingNextInterestProfilesPage,
error: interestProfilesError,
fetchNextPage: fetchNextInterestProfilesPage,
} = useActorSearchPaginated({
query: selectedInterest || '',
enabled: !!selectedInterest,
limit: 10,
})
const {isReady: canShowSuggestedProfiles} = useLoadEnoughProfiles({
interest: selectedInterest,
data: interestProfiles,
isLoading: isLoadingInterestProfiles,
isFetchingNextPage: isFetchingNextInterestProfilesPage,
hasNextPage: hasNextInterestProfilesPage,
fetchNextPage: fetchNextInterestProfilesPage,
}) })
const { const {
data: feeds, data: feeds,
@@ -233,40 +219,10 @@ export function Explore({
error: feedsError, error: feedsError,
fetchNextPage: fetchNextFeedsPage, fetchNextPage: fetchNextFeedsPage,
} = useGetPopularFeedsQuery({limit: 10}) } = useGetPopularFeedsQuery({limit: 10})
const interestsNux = useNux(Nux.ExploreInterestsCard)
const showInterestsNux =
interestsNux.status === 'ready' && !interestsNux.nux?.completed
const profiles: typeof suggestedProfiles & typeof interestProfiles =
!selectedInterest ? suggestedProfiles : interestProfiles
const hasNextProfilesPage = !selectedInterest
? hasNextSuggestedProfilesPage
: hasNextInterestProfilesPage
const isLoadingProfiles = !selectedInterest
? isLoadingSuggestedProfiles
: !canShowSuggestedProfiles
const isFetchingNextProfilesPage = !selectedInterest
? isFetchingNextSuggestedProfilesPage
: !canShowSuggestedProfiles
const profilesError = !selectedInterest
? suggestedProfilesError
: interestProfilesError
const fetchNextProfilesPage = !selectedInterest
? fetchNextSuggestedProfilesPage
: fetchNextInterestProfilesPage
const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles
const onLoadMoreProfiles = useCallback(async () => {
if (isFetchingNextProfilesPage || !hasNextProfilesPage || profilesError)
return
try {
await fetchNextProfilesPage()
} catch (err) {
logger.error('Failed to load more suggested follows', {message: err})
}
}, [
isFetchingNextProfilesPage,
hasNextProfilesPage,
profilesError,
fetchNextProfilesPage,
])
const { const {
data: suggestedSPs, data: suggestedSPs,
isLoading: isLoadingSuggestedSPs, isLoading: isLoadingSuggestedSPs,
@@ -349,55 +305,42 @@ export function Explore({
}, },
}) })
if (!canShowSuggestedProfiles) { if (suggestedUsersIsLoading) {
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
} else if (profilesError) { } else if (suggestedUsersError) {
i.push({ i.push({
type: 'error', type: 'error',
key: 'profilesError', key: 'suggestedUsersError',
message: _(msg`Failed to load suggested follows`), message: _(msg`Failed to load suggested follows`),
error: cleanError(profilesError), error: cleanError(suggestedUsersError),
}) })
} else { } else {
if (profiles !== undefined) { if (suggestedUsers !== undefined) {
if (profiles.pages.length > 0 && moderationOpts) { if (suggestedUsers.actors.length > 0 && moderationOpts) {
// Currently the responses contain duplicate items. // Currently the responses contain duplicate items.
// Needs to be fixed on backend, but let's dedupe to be safe. // Needs to be fixed on backend, but let's dedupe to be safe.
let seen = new Set() let seen = new Set()
const profileItems: ExploreScreenItems[] = [] const profileItems: ExploreScreenItems[] = []
for (const page of profiles.pages) { for (const actor of suggestedUsers.actors) {
for (const actor of page.actors) { if (!seen.has(actor.did) && !actor.viewer?.following) {
if (!seen.has(actor.did) && !actor.viewer?.following) { seen.add(actor.did)
seen.add(actor.did) profileItems.push({
profileItems.push({ type: 'profile',
type: 'profile', key: actor.did,
key: actor.did, profile: actor,
profile: actor, })
recId: page.recId,
})
}
} }
} }
if (profileItems.length === 0) { if (profileItems.length === 0) {
if (!hasNextProfilesPage) { // no items! remove the header
// no items! remove the header i.pop()
i.pop()
}
} else { } else {
i.push(...profileItems) i.push(...profileItems)
} }
if (hasNextProfilesPage) {
i.push({
type: 'loadMore',
key: 'loadMoreProfiles',
message: _(msg`Load more suggested accounts`),
isLoadingMore: isLoadingMoreProfiles,
onLoadMore: onLoadMoreProfiles,
})
}
} else { } else {
console.log('no pages') // no items! remove the header
i.pop()
} }
} else { } else {
i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'}) i.push({type: 'profilePlaceholder', key: 'profilePlaceholder'})
@@ -405,14 +348,11 @@ export function Explore({
} }
return i return i
}, [ }, [
profiles,
_, _,
canShowSuggestedProfiles,
hasNextProfilesPage,
isLoadingMoreProfiles,
moderationOpts, moderationOpts,
onLoadMoreProfiles, suggestedUsers,
profilesError, suggestedUsersIsLoading,
suggestedUsersError,
]) ])
const suggestedFeedsModule = useMemo(() => { const suggestedFeedsModule = useMemo(() => {
const i: ExploreScreenItems[] = [] const i: ExploreScreenItems[] = []
@@ -523,6 +463,7 @@ export function Explore({
key: 'suggested-starterPacks-header', key: 'suggested-starterPacks-header',
title: _(msg`Starter Packs`), title: _(msg`Starter Packs`),
icon: StarterPack, icon: StarterPack,
iconSize: 'xl',
}) })
if (isLoadingSuggestedSPs) { if (isLoadingSuggestedSPs) {
@@ -558,6 +499,16 @@ export function Explore({
return i return i
}, [feedPreviewSlices, isFetchingNextPageFeedPreviews]) }, [feedPreviewSlices, isFetchingNextPageFeedPreviews])
const interestsNuxModule = useMemo<ExploreScreenItems[]>(() => {
if (!showInterestsNux) return []
return [
{
type: 'interests-card',
key: 'interests-card',
},
]
}, [showInterestsNux])
const isNewUser = guide?.guide === 'follow-10' && !guide.isComplete const isNewUser = guide?.guide === 'follow-10' && !guide.isComplete
const items = useMemo<ExploreScreenItems[]>(() => { const items = useMemo<ExploreScreenItems[]>(() => {
const i: ExploreScreenItems[] = [] const i: ExploreScreenItems[] = []
@@ -565,6 +516,7 @@ export function Explore({
// Dynamic module ordering // Dynamic module ordering
i.push(topBorder) i.push(topBorder)
i.push(...interestsNuxModule)
if (isNewUser) { if (isNewUser) {
i.push(...suggestedFollowsModule) i.push(...suggestedFollowsModule)
i.push(...suggestedStarterPacksModule) i.push(...suggestedStarterPacksModule)
@@ -588,6 +540,7 @@ export function Explore({
suggestedFeedsModule, suggestedFeedsModule,
trendingTopicsModule, trendingTopicsModule,
feedPreviewsModule, feedPreviewsModule,
interestsNuxModule,
gate, gate,
]) ])
@@ -612,7 +565,7 @@ export function Explore({
case 'header': { case 'header': {
return ( return (
<ModuleHeader.Container> <ModuleHeader.Container>
<ModuleHeader.Icon icon={item.icon} /> <ModuleHeader.Icon icon={item.icon} size={item.iconSize} />
<ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText> <ModuleHeader.TitleText>{item.title}</ModuleHeader.TitleText>
{item.searchButton && ( {item.searchButton && (
<ModuleHeader.SearchButton <ModuleHeader.SearchButton
@@ -854,6 +807,9 @@ export function Explore({
/> />
) )
} }
case 'interests-card': {
return <ExploreInterestsCard />
}
} }
}, },
[ [
+1 -1
View File
@@ -318,7 +318,7 @@ export function SearchScreenShell({
</Layout.Header.Outer> </Layout.Header.Outer>
</View> </View>
)} )}
<View style={[a.px_md, a.pt_sm, a.pb_sm, a.overflow_hidden]}> <View style={[a.px_lg, a.pt_sm, a.pb_sm, a.overflow_hidden]}>
<View style={[a.gap_sm]}> <View style={[a.gap_sm]}>
<View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}> <View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs]}>
<View style={[a.flex_1]}> <View style={[a.flex_1]}>
+2 -10
View File
@@ -6,14 +6,7 @@ import {PressableScale} from '#/lib/custom-animations/PressableScale'
import {makeCustomFeedLink} from '#/lib/routes/links' import {makeCustomFeedLink} from '#/lib/routes/links'
import {logger} from '#/logger' import {logger} from '#/logger'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
import { import {atoms as a, native, useTheme, type ViewStyleProp, web} from '#/alf'
atoms as a,
native,
useGutters,
useTheme,
type ViewStyleProp,
web,
} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import * as FeedCard from '#/components/FeedCard' import * as FeedCard from '#/components/FeedCard'
import {sizes as iconSizes} from '#/components/icons/common' import {sizes as iconSizes} from '#/components/icons/common'
@@ -27,13 +20,12 @@ export function Container({
headerHeight, headerHeight,
}: {children: React.ReactNode; headerHeight?: number} & ViewStyleProp) { }: {children: React.ReactNode; headerHeight?: number} & ViewStyleProp) {
const t = useTheme() const t = useTheme()
const gutters = useGutters([0, 'base'])
return ( return (
<View <View
style={[ style={[
gutters,
a.flex_row, a.flex_row,
a.align_center, a.align_center,
a.px_lg,
a.pt_2xl, a.pt_2xl,
a.pb_md, a.pb_md,
a.gap_sm, a.gap_sm,
@@ -234,7 +234,7 @@ export function AvatarStack({
{computedTotal > 0 ? ( {computedTotal > 0 ? (
<Text <Text
style={[ style={[
gtPhone ? a.text_md : a.text_sm, gtPhone ? a.text_md : a.text_xs,
a.font_bold, a.font_bold,
a.leading_snug, a.leading_snug,
{color: 'white'}, {color: 'white'},
@@ -0,0 +1,127 @@
import {useState} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Nux, useSaveNux} from '#/state/queries/nuxs'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {Link} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import {Text} from '#/components/Typography'
export function ExploreInterestsCard() {
const t = useTheme()
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const interestsDisplayNames = useInterestsDisplayNames()
const {mutateAsync: saveNux} = useSaveNux()
const trendingPrompt = Prompt.usePromptControl()
const [closing, setClosing] = useState(false)
const onClose = () => {
trendingPrompt.open()
}
const onConfirmClose = () => {
setClosing(true)
// if this fails, they can try again later
saveNux({
id: Nux.ExploreInterestsCard,
completed: true,
data: undefined,
}).catch(() => {})
}
return closing ? null : (
<>
<Prompt.Basic
control={trendingPrompt}
title={_(msg`Your interests`)}
description={_(
msg`You can adjust your interests at any time from your "Content and media" settings.`,
)}
confirmButtonCta={_(
msg({
message: `Copy that!`,
comment: `Confirm button text. Can be a short cheeky phrase that means "OK" e.g. "Copy that!"`,
}),
)}
onConfirm={onConfirmClose}
/>
<View style={[a.p_lg, a.pb_2xs]}>
<View
style={[
a.p_lg,
a.rounded_md,
a.border,
a.gap_sm,
t.atoms.border_contrast_medium,
t.atoms.bg_contrast_25,
]}>
<Text style={[a.text_md, a.font_bold, a.leading_tight]}>
<Trans>Your interests</Trans>
</Text>
{preferences?.interests?.tags &&
preferences.interests.tags.length > 0 ? (
<View style={[a.flex_row, a.flex_wrap, {gap: 6}]}>
{preferences.interests.tags.map(tag => (
<View
key={tag}
style={[
a.justify_center,
a.align_center,
a.rounded_full,
a.border,
t.atoms.border_contrast_medium,
a.px_lg,
{height: 32},
]}>
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
{interestsDisplayNames[tag]}
</Text>
</View>
))}
</View>
) : null}
<Text style={[a.text_sm, a.leading_snug, a.pb_xs]}>
<Trans>
Your selected interests help us serve you content you care about.
</Trans>
</Text>
<Link
label={_(msg`Edit interests`)}
to="/settings/interests"
size="small"
variant="solid"
color="primary"
style={[a.justify_center]}>
<ButtonText>
<Trans>Edit interests</Trans>
</ButtonText>
</Link>
<Button
label={_(msg`Hide this card`)}
size="small"
variant="solid"
color="secondary"
shape="round"
onPress={onClose}
style={[
a.absolute,
{top: a.pt_xs.paddingTop, right: a.pr_xs.paddingRight},
]}>
<ButtonIcon icon={X} />
</Button>
</View>
</View>
</>
)
}
@@ -83,10 +83,16 @@ export function SuggestedAccountsTabBar({
}} }}
hasSearchText={false} hasSearchText={false}
interestsDisplayNames={{ interestsDisplayNames={{
all: _(msg`All`), all: _(msg`For You`),
...interestsDisplayNames, ...interestsDisplayNames,
}} }}
TabComponent={Tab} TabComponent={Tab}
contentContainerStyle={[
{
// visual alignment
paddingLeft: a.px_md.paddingLeft,
},
]}
/> />
</BlockDrawerGesture> </BlockDrawerGesture>
) )
+29 -21
View File
@@ -16,6 +16,7 @@ import {useAgent} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {useInterestsDisplayNames} from '#/screens/Onboarding/state' import {useInterestsDisplayNames} from '#/screens/Onboarding/state'
import {atoms as a, useGutters, useTheme} from '#/alf' import {atoms as a, useGutters, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import * as Toggle from '#/components/forms/Toggle' import * as Toggle from '#/components/forms/Toggle'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
@@ -49,8 +50,7 @@ export function SettingsInterests() {
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
]}> ]}>
<Trans> <Trans>
Selecting interests from the list below helps us deliver you Your selected interests help us serve you content you care about.
higher quality content.
</Trans> </Trans>
</Text> </Text>
@@ -135,25 +135,33 @@ function Inner({
} }
return ( return (
<Toggle.Group <>
values={interests} {interests.length === 0 && (
onChange={onChangeInterests} <Admonition type="tip">
label={_(msg`Select your interests from the options below`)}> <Trans>We recommend selecting at least two interests.</Trans>
<View style={[a.flex_row, a.flex_wrap, a.gap_sm]}> </Admonition>
{INTERESTS.map(interest => { )}
const name = interestsDisplayNames[interest]
if (!name) return null <Toggle.Group
return ( values={interests}
<Toggle.Item onChange={onChangeInterests}
key={interest} label={_(msg`Select your interests from the options below`)}>
name={interest} <View style={[a.flex_row, a.flex_wrap, a.gap_sm]}>
label={interestsDisplayNames[interest]}> {INTERESTS.map(interest => {
<InterestButton interest={interest} /> const name = interestsDisplayNames[interest]
</Toggle.Item> if (!name) return null
) return (
})} <Toggle.Item
</View> key={interest}
</Toggle.Group> name={interest}
label={interestsDisplayNames[interest]}>
<InterestButton interest={interest} />
</Toggle.Item>
)
})}
</View>
</Toggle.Group>
</>
) )
} }
+2
View File
@@ -19,6 +19,7 @@ import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '#
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers' import {findAllProfilesInQueryData as findAllProfilesInProfileFollowersQueryData} from '#/state/queries/profile-followers'
import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows' import {findAllProfilesInQueryData as findAllProfilesInProfileFollowsQueryData} from '#/state/queries/profile-follows'
import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows' import {findAllProfilesInQueryData as findAllProfilesInSuggestedFollowsQueryData} from '#/state/queries/suggested-follows'
import {findAllProfilesInQueryData as findAllProfilesInSuggestedUsersQueryData} from '#/state/queries/trending/useGetSuggestedUsersQuery'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
import {castAsShadow, type Shadow} from './types' import {castAsShadow, type Shadow} from './types'
@@ -149,6 +150,7 @@ function* findProfilesInCache(
yield* findAllProfilesInProfileQueryData(queryClient, did) yield* findAllProfilesInProfileQueryData(queryClient, did)
yield* findAllProfilesInProfileFollowersQueryData(queryClient, did) yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
yield* findAllProfilesInProfileFollowsQueryData(queryClient, did) yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
yield* findAllProfilesInSuggestedUsersQueryData(queryClient, did)
yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did) yield* findAllProfilesInSuggestedFollowsQueryData(queryClient, did)
yield* findAllProfilesInActorSearchQueryData(queryClient, did) yield* findAllProfilesInActorSearchQueryData(queryClient, did)
yield* findAllProfilesInListConvosQueryData(queryClient, did) yield* findAllProfilesInListConvosQueryData(queryClient, did)
+14 -6
View File
@@ -1,18 +1,26 @@
import zod from 'zod' import type zod from 'zod'
import {BaseNux} from '#/state/queries/nuxs/types' import {type BaseNux} from '#/state/queries/nuxs/types'
export enum Nux { export enum Nux {
NeueTypography = 'NeueTypography', NeueTypography = 'NeueTypography',
ExploreInterestsCard = 'ExploreInterestsCard',
} }
export const nuxNames = new Set(Object.values(Nux)) export const nuxNames = new Set(Object.values(Nux))
export type AppNux = BaseNux<{ export type AppNux = BaseNux<
id: Nux.NeueTypography | {
data: undefined id: Nux.NeueTypography
}> data: undefined
}
| {
id: Nux.ExploreInterestsCard
data: undefined
}
>
export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = { export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
[Nux.NeueTypography]: undefined, [Nux.NeueTypography]: undefined,
[Nux.ExploreInterestsCard]: undefined,
} }
@@ -0,0 +1,71 @@
import {
type AppBskyActorDefs,
type AppBskyUnspeccedGetSuggestedUsers,
} from '@atproto/api'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {
aggregateUserInterests,
createBskyTopicsHeader,
} from '#/lib/api/feed/utils'
import {getContentLanguages} from '#/state/preferences/languages'
import {STALE} from '#/state/queries'
import {usePreferencesQuery} from '#/state/queries/preferences'
import {useAgent} from '#/state/session'
export type QueryProps = {category?: string | null}
export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users'
export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [
getSuggestedUsersQueryKeyRoot,
...Object.values(props),
]
export function useGetSuggestedUsersQuery(props: QueryProps) {
const agent = useAgent()
const {data: preferences} = usePreferencesQuery()
return useQuery({
enabled: !!preferences,
refetchOnWindowFocus: true,
staleTime: STALE.MINUTES.ONE,
queryKey: createGetSuggestedUsersQueryKey(props),
queryFn: async () => {
const contentLangs = getContentLanguages().join(',')
const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
{
category: props.category ?? undefined,
},
{
headers: {
...createBskyTopicsHeader(aggregateUserInterests(preferences)),
'Accept-Language': contentLangs,
},
},
)
return data
},
})
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<AppBskyActorDefs.ProfileViewBasic, void> {
const responses =
queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsers.OutputSchema>({
queryKey: [getSuggestedUsersQueryKeyRoot],
})
for (const [_, response] of responses) {
if (!response) {
continue
}
for (const actor of response.actors) {
if (actor.did === did) {
yield actor
}
}
}
}
+4 -4
View File
@@ -80,10 +80,10 @@
tlds "^1.234.0" tlds "^1.234.0"
zod "^3.23.8" zod "^3.23.8"
"@atproto/api@^0.14.19": "@atproto/api@^0.14.20":
version "0.14.19" version "0.14.20"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.19.tgz#fef8994e2b14e69a9e3a0aef043c7fcb34d6bf8c" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.14.20.tgz#904c85a91748f3203fd929415cb8fb3bc78d35d3"
integrity sha512-YYTqM0K0qk2TP7PguktPzlAQGLTL1bEGz6PgY5kqKJNX4o1318kJYB22DzjJYqV2NUCq0JQ9Lb0oskLvTisEOg== integrity sha512-Daip22+u9N+EVPk9PsEEVrTfjIqGczXnAT7o2EHGd0JsOzMbp3a6wmW1beKqYDzPf+Dc36/39JeUYYqhB3fKjg==
dependencies: dependencies:
"@atproto/common-web" "^0.4.1" "@atproto/common-web" "^0.4.1"
"@atproto/lexicon" "^0.4.10" "@atproto/lexicon" "^0.4.10"