Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #8733 from internet-development/caidan/app-1330-new-design-for-suggested-for-you-interstitial

feat: new design for "Suggested for you" interstitial

authored by

jim and committed by
GitHub
c31b0338 332dbc1b

+161 -85
+125 -76
src/components/FeedInterstitials.tsx
··· 25 25 type ViewStyleProp, 26 26 web, 27 27 } from '#/alf' 28 - import {Button} from '#/components/Button' 28 + import {Button, ButtonText} from '#/components/Button' 29 29 import * as FeedCard from '#/components/FeedCard' 30 30 import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' 31 31 import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' 32 - import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' 33 32 import {InlineLinkText} from '#/components/Link' 34 33 import * as ProfileCard from '#/components/ProfileCard' 35 34 import {Text} from '#/components/Typography' 36 35 import type * as bsky from '#/types/bsky' 37 36 import {ProgressGuideList} from './ProgressGuide/List' 38 37 39 - const MOBILE_CARD_WIDTH = 300 38 + const MOBILE_CARD_WIDTH = 165 40 39 41 40 function CardOuter({ 42 41 children, ··· 48 47 <View 49 48 style={[ 50 49 a.w_full, 51 - a.p_lg, 52 - a.rounded_md, 50 + a.p_md, 51 + a.rounded_lg, 53 52 a.border, 54 53 t.atoms.bg, 55 54 t.atoms.border_contrast_low, ··· 65 64 66 65 export function SuggestedFollowPlaceholder() { 67 66 const t = useTheme() 67 + 68 68 return ( 69 - <CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}> 70 - <ProfileCard.Header> 71 - <ProfileCard.AvatarPlaceholder /> 72 - <ProfileCard.NameAndHandlePlaceholder /> 73 - </ProfileCard.Header> 69 + <CardOuter 70 + style={[a.gap_md, t.atoms.border_contrast_low, t.atoms.shadow_sm]}> 71 + <ProfileCard.Outer> 72 + <View 73 + style={[a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto]}> 74 + <ProfileCard.AvatarPlaceholder size={88} /> 75 + <ProfileCard.NamePlaceholder /> 76 + <View style={[a.w_full]}> 77 + <ProfileCard.DescriptionPlaceholder numberOfLines={2} /> 78 + </View> 79 + </View> 74 80 75 - <ProfileCard.DescriptionPlaceholder numberOfLines={2} /> 81 + <Button 82 + label="" 83 + size="small" 84 + variant="solid" 85 + color="secondary" 86 + disabled 87 + style={[a.w_full, a.rounded_sm]}> 88 + <ButtonText>Follow</ButtonText> 89 + </Button> 90 + </ProfileCard.Outer> 76 91 </CardOuter> 77 92 ) 78 93 } ··· 243 258 const t = useTheme() 244 259 const {_} = useLingui() 245 260 const moderationOpts = useModerationOpts() 246 - const navigation = useNavigation<NavigationProp>() 247 261 const {gtMobile} = useBreakpoints() 248 262 const isLoading = isSuggestionsLoading || !moderationOpts 249 - const maxLength = gtMobile ? 4 : 6 263 + const maxLength = gtMobile ? 3 : 6 250 264 251 265 const content = isLoading ? ( 252 266 Array(maxLength) ··· 254 268 .map((_, i) => ( 255 269 <View 256 270 key={i} 257 - style={[gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}])]}> 271 + style={[ 272 + gtMobile && 273 + web([ 274 + a.flex_0, 275 + a.flex_grow, 276 + {width: `calc(30% - ${a.gap_md.gap / 2}px)`}, 277 + ]), 278 + ]}> 258 279 <SuggestedFollowPlaceholder /> 259 280 </View> 260 281 )) ··· 276 297 }} 277 298 style={[ 278 299 a.flex_1, 279 - gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]), 300 + gtMobile && 301 + web([ 302 + a.flex_0, 303 + a.flex_grow, 304 + {width: `calc(30% - ${a.gap_md.gap / 2}px)`}, 305 + ]), 280 306 ]}> 281 307 {({hovered, pressed}) => ( 282 308 <CardOuter 283 309 style={[ 284 310 a.flex_1, 311 + t.atoms.shadow_sm, 285 312 (hovered || pressed) && t.atoms.border_contrast_high, 286 313 ]}> 287 314 <ProfileCard.Outer> 288 - <ProfileCard.Header> 315 + <View 316 + style={[ 317 + a.flex_col, 318 + a.align_center, 319 + a.gap_sm, 320 + a.pb_sm, 321 + a.mb_auto, 322 + ]}> 289 323 <ProfileCard.Avatar 290 324 profile={profile} 291 325 moderationOpts={moderationOpts} 326 + size={88} 292 327 /> 293 - <ProfileCard.NameAndHandle 294 - profile={profile} 295 - moderationOpts={moderationOpts} 296 - /> 297 - <ProfileCard.FollowButton 298 - profile={profile} 299 - moderationOpts={moderationOpts} 300 - logContext="FeedInterstitial" 301 - shape="round" 302 - colorInverted 303 - onFollow={() => { 304 - logEvent('suggestedUser:follow', { 305 - logContext: 306 - viewContext === 'feed' 307 - ? 'InterstitialDiscover' 308 - : 'InterstitialProfile', 309 - location: 'Card', 310 - recId, 311 - position: index, 312 - }) 313 - }} 314 - /> 315 - </ProfileCard.Header> 316 - <ProfileCard.Description profile={profile} numberOfLines={2} /> 328 + <View style={[a.flex_col, a.align_center, a.max_w_full]}> 329 + <ProfileCard.Name 330 + profile={profile} 331 + moderationOpts={moderationOpts} 332 + /> 333 + <ProfileCard.Description 334 + profile={profile} 335 + numberOfLines={2} 336 + style={[ 337 + t.atoms.text_contrast_medium, 338 + a.text_center, 339 + a.text_xs, 340 + ]} 341 + /> 342 + </View> 343 + </View> 344 + 345 + <ProfileCard.FollowButton 346 + profile={profile} 347 + moderationOpts={moderationOpts} 348 + logContext="FeedInterstitial" 349 + withIcon={false} 350 + style={[a.rounded_sm]} 351 + onFollow={() => { 352 + logEvent('suggestedUser:follow', { 353 + logContext: 354 + viewContext === 'feed' 355 + ? 'InterstitialDiscover' 356 + : 'InterstitialProfile', 357 + location: 'Card', 358 + recId, 359 + position: index, 360 + }) 361 + }} 362 + /> 317 363 </ProfileCard.Outer> 318 364 </CardOuter> 319 365 )} ··· 333 379 <View 334 380 style={[ 335 381 a.p_lg, 336 - a.pb_xs, 382 + a.py_md, 337 383 a.flex_row, 338 384 a.align_center, 339 385 a.justify_between, 340 386 ]}> 341 - <Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}> 387 + <Text style={[a.text_sm, a.font_bold, t.atoms.text]}> 342 388 {viewContext === 'profile' ? ( 343 389 <Trans>Similar accounts</Trans> 344 390 ) : ( 345 391 <Trans>Suggested for you</Trans> 346 392 )} 347 393 </Text> 348 - <Person fill={t.atoms.text_contrast_low.color} size="sm" /> 394 + <InlineLinkText 395 + label={_(msg`See more suggested profiles on the Explore page`)} 396 + to="/search"> 397 + <Trans>See more</Trans> 398 + </InlineLinkText> 349 399 </View> 350 400 351 401 {gtMobile ? ( 352 - <View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}> 353 - <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}> 402 + <View style={[a.px_lg, a.pb_lg]}> 403 + <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}> 354 404 {content} 355 405 </View> 356 - 357 - <View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}> 358 - <InlineLinkText 359 - label={_(msg`Browse more suggestions`)} 360 - to="/search" 361 - style={[t.atoms.text_contrast_medium]}> 362 - <Trans>Browse more suggestions</Trans> 363 - </InlineLinkText> 364 - <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} /> 365 - </View> 366 406 </View> 367 407 ) : ( 368 408 <BlockDrawerGesture> ··· 371 411 horizontal 372 412 showsHorizontalScrollIndicator={false} 373 413 snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap} 374 - decelerationRate="fast"> 375 - <View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}> 414 + decelerationRate="fast" 415 + style={[a.overflow_visible]}> 416 + <View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}> 376 417 {content} 377 418 378 - <Button 379 - label={_(msg`Browse more accounts on the Explore page`)} 380 - onPress={() => { 381 - navigation.navigate('SearchTab') 382 - }}> 383 - <CardOuter style={[a.flex_1, {borderWidth: 0}]}> 384 - <View style={[a.flex_1, a.justify_center]}> 385 - <View style={[a.flex_row, a.px_lg]}> 386 - <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}> 387 - <Trans> 388 - Browse more suggestions on the Explore page 389 - </Trans> 390 - </Text> 391 - 392 - <Arrow size="xl" /> 393 - </View> 394 - </View> 395 - </CardOuter> 396 - </Button> 419 + <SeeMoreSuggestedProfilesCard /> 397 420 </View> 398 421 </ScrollView> 399 422 </View> 400 423 </BlockDrawerGesture> 401 424 )} 402 425 </View> 426 + ) 427 + } 428 + 429 + function SeeMoreSuggestedProfilesCard() { 430 + const navigation = useNavigation<NavigationProp>() 431 + const t = useTheme() 432 + const {_} = useLingui() 433 + 434 + return ( 435 + <Button 436 + label={_(msg`Browse more accounts on the Explore page`)} 437 + onPress={() => { 438 + navigation.navigate('SearchTab') 439 + }}> 440 + <CardOuter style={[a.flex_1, t.atoms.shadow_sm]}> 441 + <View style={[a.flex_1, a.justify_center]}> 442 + <View style={[a.flex_col, a.align_center, a.gap_md]}> 443 + <Text style={[a.leading_snug, a.text_center]}> 444 + <Trans>See more accounts you might like</Trans> 445 + </Text> 446 + 447 + <Arrow size="xl" /> 448 + </View> 449 + </View> 450 + </CardOuter> 451 + </Button> 403 452 ) 404 453 } 405 454
+36 -9
src/components/ProfileCard.tsx
··· 20 20 import {useSession} from '#/state/session' 21 21 import * as Toast from '#/view/com/util/Toast' 22 22 import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar' 23 - import {atoms as a, platform, useTheme} from '#/alf' 23 + import { 24 + atoms as a, 25 + platform, 26 + type TextStyleProp, 27 + useTheme, 28 + type ViewStyleProp, 29 + } from '#/alf' 24 30 import { 25 31 Button, 26 32 ButtonIcon, ··· 136 142 onPress, 137 143 disabledPreview, 138 144 liveOverride, 145 + size = 40, 139 146 }: { 140 147 profile: bsky.profile.AnyProfileView 141 148 moderationOpts: ModerationOpts 142 149 onPress?: () => void 143 150 disabledPreview?: boolean 144 151 liveOverride?: boolean 152 + size?: number 145 153 }) { 146 154 const moderation = moderateProfile(profile, moderationOpts) 147 155 ··· 149 157 150 158 return disabledPreview ? ( 151 159 <UserAvatar 152 - size={40} 160 + size={size} 153 161 avatar={profile.avatar} 154 162 type={profile.associated?.labeler ? 'labeler' : 'user'} 155 163 moderation={moderation.ui('avatar')} ··· 157 165 /> 158 166 ) : ( 159 167 <PreviewableUserAvatar 160 - size={40} 168 + size={size} 161 169 profile={profile} 162 170 moderation={moderation.ui('avatar')} 163 171 onBeforePress={onPress} ··· 166 174 ) 167 175 } 168 176 169 - export function AvatarPlaceholder() { 177 + export function AvatarPlaceholder({size = 40}: {size?: number}) { 170 178 const t = useTheme() 171 179 return ( 172 180 <View ··· 174 182 a.rounded_full, 175 183 t.atoms.bg_contrast_25, 176 184 { 177 - width: 40, 178 - height: 40, 185 + width: size, 186 + height: size, 179 187 }, 180 188 ]} 181 189 /> ··· 274 282 ) 275 283 const verification = useSimpleVerificationState({profile}) 276 284 return ( 277 - <View style={[a.flex_row, a.align_center]}> 285 + <View style={[a.flex_row, a.align_center, a.max_w_full]}> 278 286 <Text 279 287 emoji 280 288 style={[ ··· 343 351 ) 344 352 } 345 353 354 + export function NamePlaceholder({style}: ViewStyleProp) { 355 + const t = useTheme() 356 + 357 + return ( 358 + <View 359 + style={[ 360 + a.rounded_xs, 361 + t.atoms.bg_contrast_25, 362 + { 363 + width: '60%', 364 + height: 14, 365 + }, 366 + style, 367 + ]} 368 + /> 369 + ) 370 + } 371 + 346 372 export function Description({ 347 373 profile: profileUnshadowed, 348 374 numberOfLines = 3, 375 + style, 349 376 }: { 350 377 profile: bsky.profile.AnyProfileView 351 378 numberOfLines?: number 352 - }) { 379 + } & TextStyleProp) { 353 380 const profile = useProfileShadow(profileUnshadowed) 354 381 const rt = useMemo(() => { 355 382 if (!('description' in profile)) return ··· 369 396 <View style={[a.pt_xs]}> 370 397 <RichText 371 398 value={rt} 372 - style={[a.leading_snug]} 399 + style={[a.leading_snug, style]} 373 400 numberOfLines={numberOfLines} 374 401 disableLinks 375 402 />