Getting started logic now checks net follow count instead of total follow events

This commit is contained in:
Alex Benzer
2025-12-31 14:25:33 -08:00
committed by Samuel Newman
parent 6bcb5236d9
commit 599e92861d
6 changed files with 181 additions and 50 deletions
@@ -32,7 +32,6 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass'
import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {boostInterests, InterestTabs} from '#/components/InterestTabs'
import * as ProfileCard from '#/components/ProfileCard'
@@ -77,7 +76,6 @@ export function FollowDialog({guide}: {guide: Follow10ProgressGuide}) {
size={gtMobile ? 'small' : 'large'}
color="primary"
variant="solid">
<ButtonIcon icon={PersonGroupIcon} />
<ButtonText>
<Trans>Find people to follow</Trans>
</ButtonText>
+86 -9
View File
@@ -2,24 +2,41 @@ import {type StyleProp, View, type ViewStyle} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useSession} from '#/state/session'
import {
useProgressGuide,
useProgressGuideControls,
} from '#/state/shell/progress-guide'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
import {FollowDialog} from './FollowDialog'
import {ProgressGuideTask} from './Task'
const TOTAL_AVATARS = 10
export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
const t = useTheme()
const {_} = useLingui()
const {currentAccount} = useSession()
const followProgressGuide = useProgressGuide('follow-10')
const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7')
const guide = followProgressGuide || followAndLikeProgressGuide
const {endProgressGuide} = useProgressGuideControls()
const {data: follows} = useProfileFollowsQuery(currentAccount?.did, {
limit: TOTAL_AVATARS,
})
const actualFollowsCount = follows?.pages?.[0]?.follows?.length ?? 0
// Hide if user already follows 10+ people
if (guide?.guide === 'follow-10' && actualFollowsCount >= TOTAL_AVATARS) {
return null
}
if (guide) {
return (
@@ -36,9 +53,8 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
a.pl_md,
]}>
<View style={[a.flex_row, a.align_center, a.justify_between]}>
<Text
style={[t.atoms.text_contrast_medium, a.font_semi_bold, a.text_sm]}>
<Trans>Getting Started</Trans>
<Text style={[t.atoms.text, a.font_semi_bold, a.text_md]}>
<Trans>Follow 10 people to get started</Trans>
</Text>
<Button
variant="ghost"
@@ -53,12 +69,7 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
</View>
{guide.guide === 'follow-10' && (
<>
<ProgressGuideTask
current={guide.numFollows + 1}
total={10 + 1}
title={_(msg`Follow 10 accounts`)}
subtitle={_(msg`Bluesky is better with friends!`)}
/>
<StackedAvatars follows={follows?.pages?.[0]?.follows} />
<FollowDialog guide={guide} />
</>
)}
@@ -83,3 +94,69 @@ export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
}
return null
}
function StackedAvatars({follows}: {follows?: {avatar?: string}[]}) {
const t = useTheme()
const avatarSize = 38
const overlap = 11
// Use actual follows count, not the guide's event counter
const followedAvatars = follows?.slice(0, TOTAL_AVATARS) ?? []
const remainingSlots = TOTAL_AVATARS - followedAvatars.length
// Total width calculation: first avatar + (remaining * visible portion)
const totalWidth = avatarSize + (TOTAL_AVATARS - 1) * (avatarSize - overlap)
return (
<View style={[a.flex_row, a.self_start, {width: totalWidth}]}>
{/* Show followed user avatars */}
{followedAvatars.map((follow, i) => (
<View
key={i}
style={[
a.rounded_full,
{
marginLeft: i === 0 ? 0 : -overlap,
zIndex: TOTAL_AVATARS - i,
borderWidth: 2,
borderColor: t.atoms.bg_contrast_25.backgroundColor,
},
]}>
<UserAvatar
type="user"
size={avatarSize - 4}
avatar={follow.avatar}
/>
</View>
))}
{/* Show placeholder avatars for remaining slots */}
{Array(remainingSlots)
.fill(0)
.map((_, i) => (
<View
key={`placeholder-${i}`}
style={[
a.align_center,
a.justify_center,
a.rounded_full,
t.atoms.bg_contrast_100,
{
width: avatarSize,
height: avatarSize,
marginLeft:
followedAvatars.length === 0 && i === 0 ? 0 : -overlap,
zIndex: TOTAL_AVATARS - followedAvatars.length - i,
borderWidth: 2,
borderColor: t.atoms.bg_contrast_25.backgroundColor,
},
]}>
<PersonIcon
width={18}
height={18}
fill={t.atoms.text_contrast_low.color}
/>
</View>
))}
</View>
)
}
+40
View File
@@ -26,6 +26,7 @@ import {type Shadow} from '#/state/cache/types'
import {type ImageMeta} from '#/state/gallery'
import {STALE} from '#/state/queries'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {RQKEY as PROFILE_FOLLOWS_RQKEY} from '#/state/queries/profile-follows'
import {
unstableCacheProfileView,
useUnstableProfileViewCache,
@@ -247,6 +248,7 @@ export function useProfileFollowMutationQueue(
) {
const agent = useAgent()
const queryClient = useQueryClient()
const {currentAccount} = useSession()
const did = profile.did
const initialFollowingUri = profile.viewer?.following
const followMutation = useProfileFollowMutation(
@@ -283,6 +285,44 @@ export function useProfileFollowMutationQueue(
followingUri: finalFollowingUri,
})
// Optimistically update profile follows cache for avatar displays
if (currentAccount?.did) {
queryClient.setQueryData(
PROFILE_FOLLOWS_RQKEY(currentAccount.did),
(old: any) => {
if (!old?.pages?.[0]) return old
if (finalFollowingUri) {
// Add the followed profile to the beginning
const alreadyExists = old.pages[0].follows.some(
(f: any) => f.did === profile.did,
)
if (alreadyExists) return old
return {
...old,
pages: [
{
...old.pages[0],
follows: [profile, ...old.pages[0].follows],
},
...old.pages.slice(1),
],
}
} else {
// Remove the unfollowed profile
return {
...old,
pages: old.pages.map((page: any) => ({
...page,
follows: page.follows.filter(
(f: any) => f.did !== profile.did,
),
})),
}
}
},
)
}
if (finalFollowingUri) {
agent.app.bsky.graph
.getSuggestedFollowsByActor({
+44 -35
View File
@@ -70,7 +70,7 @@ export function DesktopFeeds() {
* Small padding prevents overflow prior to actually overflowing the
* height of the screen with lots of feeds.
*/
paddingVertical: 2,
paddingTop: 2,
overflowY: 'auto',
}),
]}>
@@ -112,40 +112,50 @@ export function DesktopFeeds() {
a.self_start,
a.rounded_sm,
{paddingVertical: 6, paddingHorizontal: 8},
route.name === 'Feeds' && {backgroundColor: t.palette.primary_50},
]}>
{({hovered}) => (
<>
<View
style={[
a.align_center,
a.justify_center,
a.rounded_xs,
t.atoms.bg_contrast_50,
{
width: 20,
height: 20,
},
]}>
<Plus
style={{width: 16, height: 16}}
fill={
hovered
? t.atoms.text.color
: t.atoms.text_contrast_medium.color
}
/>
</View>
<Text
style={[
a.text_md,
a.leading_snug,
hovered ? t.atoms.text : t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{_(msg`More feeds`)}
</Text>
</>
)}
{({hovered}) => {
const isActive = route.name === 'Feeds'
return (
<>
<View
style={[
a.align_center,
a.justify_center,
a.rounded_xs,
isActive
? {backgroundColor: t.palette.primary_100}
: t.atoms.bg_contrast_50,
{
width: 20,
height: 20,
},
]}>
<Plus
style={{width: 16, height: 16}}
fill={
isActive || hovered
? t.atoms.text.color
: t.atoms.text_contrast_medium.color
}
/>
</View>
<Text
style={[
a.text_md,
a.leading_snug,
isActive
? [t.atoms.text, a.font_semi_bold]
: hovered
? t.atoms.text
: t.atoms.text_contrast_medium,
]}
numberOfLines={1}>
{_(msg`More feeds`)}
</Text>
</>
)
}}
</Link>
</View>
)
@@ -183,7 +193,6 @@ function FeedItem({
a.self_start,
a.rounded_sm,
{paddingVertical: 6, paddingHorizontal: 8},
// current && t.atoms.bg_contrast_50,
current && {backgroundColor: t.palette.primary_50},
]}>
{isFollowing ? (
+10 -4
View File
@@ -100,25 +100,31 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
email: currentAccount?.email,
handle: currentAccount?.handle,
})}
style={[t.atoms.text_contrast_medium]}
label={_(msg`Feedback`)}>
{_(msg`Feedback`)}
</InlineLinkText>
{' '}
<Text style={[t.atoms.text_contrast_low]}>{' '}</Text>
</>
)}
<InlineLinkText
to="https://bsky.social/about/support/privacy-policy"
style={[t.atoms.text_contrast_medium]}
label={_(msg`Privacy`)}>
{_(msg`Privacy`)}
</InlineLinkText>
{' '}
<Text style={[t.atoms.text_contrast_low]}>{' '}</Text>
<InlineLinkText
to="https://bsky.social/about/support/tos"
style={[t.atoms.text_contrast_medium]}
label={_(msg`Terms`)}>
{_(msg`Terms`)}
</InlineLinkText>
{' '}
<InlineLinkText label={_(msg`Help`)} to={HELP_DESK_URL}>
<Text style={[t.atoms.text_contrast_low]}>{' '}</Text>
<InlineLinkText
label={_(msg`Help`)}
to={HELP_DESK_URL}
style={[t.atoms.text_contrast_medium]}>
{_(msg`Help`)}
</InlineLinkText>
</Text>
@@ -97,6 +97,7 @@ function Inner() {
<TrendingTopicLink
key={topic.link}
topic={topic}
style={[a.self_start]}
onPress={() => {
logEvent('trendingTopic:click', {context: 'sidebar'})
}}>