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