forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {type StyleProp, View, type ViewStyle} from 'react-native'
2import {msg, Trans} from '@lingui/macro'
3import {useLingui} from '@lingui/react'
4
5import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
6import {useSession} from '#/state/session'
7import {
8 useProgressGuide,
9 useProgressGuideControls,
10} from '#/state/shell/progress-guide'
11import {UserAvatar} from '#/view/com/util/UserAvatar'
12import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf'
13import {Button, ButtonIcon} from '#/components/Button'
14import {Person_Stroke2_Corner2_Rounded as PersonIcon} from '#/components/icons/Person'
15import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
16import {Text} from '#/components/Typography'
17import type * as bsky from '#/types/bsky'
18import {FollowDialog} from './FollowDialog'
19import {ProgressGuideTask} from './Task'
20
21const TOTAL_AVATARS = 10
22
23export function ProgressGuideList({style}: {style?: StyleProp<ViewStyle>}) {
24 const t = useTheme()
25 const {_} = useLingui()
26 const {gtPhone} = useBreakpoints()
27 const {rightNavVisible} = useLayoutBreakpoints()
28 const {currentAccount} = useSession()
29 const followProgressGuide = useProgressGuide('follow-10')
30 const followAndLikeProgressGuide = useProgressGuide('like-10-and-follow-7')
31 const guide = followProgressGuide || followAndLikeProgressGuide
32 const {endProgressGuide} = useProgressGuideControls()
33 const {data: follows} = useProfileFollowsQuery(currentAccount?.did, {
34 limit: TOTAL_AVATARS,
35 })
36
37 const actualFollowsCount = follows?.pages?.[0]?.follows?.length ?? 0
38
39 // Hide if user already follows 10+ people
40 if (guide?.guide === 'follow-10' && actualFollowsCount >= TOTAL_AVATARS) {
41 return null
42 }
43
44 // Inline layout when left nav visible but no right sidebar (800-1100px)
45 const inlineLayout = gtPhone && !rightNavVisible
46
47 if (guide) {
48 return (
49 <View
50 style={[
51 a.flex_col,
52 a.gap_md,
53 a.rounded_md,
54 t.atoms.bg_contrast_25,
55 a.p_lg,
56 style,
57 ]}>
58 <View style={[a.flex_row, a.align_center, a.justify_between]}>
59 <Text style={[t.atoms.text, a.font_semi_bold, a.text_md]}>
60 <Trans>Follow 10 people to get started</Trans>
61 </Text>
62 <Button
63 variant="ghost"
64 size="tiny"
65 color="secondary"
66 shape="round"
67 label={_(msg`Dismiss getting started guide`)}
68 onPress={endProgressGuide}
69 style={[a.bg_transparent, {marginTop: -6, marginRight: -6}]}>
70 <ButtonIcon icon={Times} size="xs" />
71 </Button>
72 </View>
73 {guide.guide === 'follow-10' && (
74 <View
75 style={[
76 inlineLayout
77 ? [
78 a.flex_row,
79 a.flex_wrap,
80 a.align_center,
81 a.justify_between,
82 a.gap_sm,
83 ]
84 : a.flex_col,
85 !inlineLayout && a.gap_md,
86 ]}>
87 <StackedAvatars follows={follows?.pages?.[0]?.follows} />
88 <FollowDialog guide={guide} showArrow={inlineLayout} />
89 </View>
90 )}
91 {guide.guide === 'like-10-and-follow-7' && (
92 <>
93 <ProgressGuideTask
94 current={guide.numLikes + 1}
95 total={10 + 1}
96 title={_(msg`Like 10 skeets`)}
97 subtitle={_(msg`Teach our algorithm what you like`)}
98 />
99 <ProgressGuideTask
100 current={guide.numFollows + 1}
101 total={7 + 1}
102 title={_(msg`Follow 7 accounts`)}
103 subtitle={_(msg`Bluesky is better with friends!`)}
104 />
105 </>
106 )}
107 </View>
108 )
109 }
110 return null
111}
112
113function StackedAvatars({follows}: {follows?: bsky.profile.AnyProfileView[]}) {
114 const t = useTheme()
115 const {centerColumnOffset} = useLayoutBreakpoints()
116
117 // Smaller avatars for narrower viewport
118 const avatarSize = centerColumnOffset ? 30 : 37
119 const overlap = centerColumnOffset ? 9 : 11
120 const iconSize = centerColumnOffset ? 14 : 18
121
122 // Use actual follows count, not the guide's event counter
123 const followedAvatars = follows?.slice(0, TOTAL_AVATARS) ?? []
124 const remainingSlots = TOTAL_AVATARS - followedAvatars.length
125
126 // Total width calculation: first avatar + (remaining * visible portion)
127 const totalWidth = avatarSize + (TOTAL_AVATARS - 1) * (avatarSize - overlap)
128
129 return (
130 <View style={[a.flex_row, a.self_start, {width: totalWidth}]}>
131 {/* Show followed user avatars */}
132 {followedAvatars.map((follow, i) => (
133 <View
134 key={follow.did}
135 style={[
136 a.rounded_full,
137 {
138 marginLeft: i === 0 ? 0 : -overlap,
139 zIndex: TOTAL_AVATARS - i,
140 borderWidth: 2,
141 borderColor: t.atoms.bg_contrast_25.backgroundColor,
142 },
143 ]}>
144 <UserAvatar
145 type="user"
146 size={avatarSize - 4}
147 avatar={follow.avatar}
148 />
149 </View>
150 ))}
151 {/* Show placeholder avatars for remaining slots */}
152 {Array(remainingSlots)
153 .fill(0)
154 .map((_, i) => (
155 <View
156 key={`placeholder-${i}`}
157 style={[
158 a.align_center,
159 a.justify_center,
160 a.rounded_full,
161 t.atoms.bg_contrast_100,
162 {
163 width: avatarSize,
164 height: avatarSize,
165 marginLeft:
166 followedAvatars.length === 0 && i === 0 ? 0 : -overlap,
167 zIndex: TOTAL_AVATARS - followedAvatars.length - i,
168 borderWidth: 2,
169 borderColor: t.atoms.bg_contrast_25.backgroundColor,
170 },
171 ]}>
172 <PersonIcon
173 width={iconSize}
174 height={iconSize}
175 fill={t.atoms.text_contrast_low.color}
176 />
177 </View>
178 ))}
179 </View>
180 )
181}