Bluesky app fork with some witchin' additions 馃挮
at readme-update 181 lines 6.1 kB view raw
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}