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