Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork

Merge with upstream https://github.com/bluesky-social/social-app/

xan.lol 883ec190 f7056909

verified
+1
.gitignore
··· 128 128 bskyweb/static/media/*.webp 129 129 bskyweb/static/media/*.jpg 130 130 bskyweb/static/media/*.png 131 + bskyweb/static/media/*.svg
+1 -1
package.json
··· 1 1 { 2 2 "name": "witchsky-app", 3 - "version": "1.112.0", 3 + "version": "1.113.0", 4 4 "private": true, 5 5 "engines": { 6 6 "node": ">=18"
-2
src/App.native.tsx
··· 61 61 import {ThemeProvider as Alf} from '#/alf' 62 62 import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 63 63 import {Provider as ContextMenuProvider} from '#/components/ContextMenu' 64 - import {NuxDialogs} from '#/components/dialogs/nuxs' 65 64 import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' 66 65 import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' 67 66 import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay' ··· 165 164 <IntentDialogProvider> 166 165 <TestCtrls /> 167 166 <Shell /> 168 - <NuxDialogs /> 169 167 <ToastOutlet /> 170 168 </IntentDialogProvider> 171 169 </GlobalGestureEventsProvider>
-2
src/App.web.tsx
··· 48 48 import {ThemeProvider as Alf} from '#/alf' 49 49 import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 50 50 import {Provider as ContextMenuProvider} from '#/components/ContextMenu' 51 - import {NuxDialogs} from '#/components/dialogs/nuxs' 52 51 import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' 53 52 import {Provider as IntentDialogProvider} from '#/components/intents/IntentDialogs' 54 53 import {Provider as PolicyUpdateOverlayProvider} from '#/components/PolicyUpdateOverlay' ··· 138 137 <HideBottomBarBorderProvider> 139 138 <IntentDialogProvider> 140 139 <Shell /> 141 - <NuxDialogs /> 142 140 <ToastOutlet /> 143 141 </IntentDialogProvider> 144 142 </HideBottomBarBorderProvider>
+33 -30
src/ageAssurance/components/NoAccessScreen.tsx
··· 9 9 useCreateSupportLink, 10 10 } from '#/lib/hooks/useCreateSupportLink' 11 11 import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 12 - import {isAppPassword} from '#/lib/jwt' 13 12 import {logger} from '#/logger' 14 13 import {isWeb} from '#/platform/detection' 15 14 import {isNative} from '#/platform/detection' 16 15 import {useIsBirthdateUpdateAllowed} from '#/state/birthdate' 17 - import {useSession, useSessionApi} from '#/state/session' 16 + import {useSessionApi} from '#/state/session' 18 17 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 19 18 import {Admonition} from '#/components/Admonition' 20 19 import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' ··· 30 29 import {createStaticClick, SimpleInlineLinkText} from '#/components/Link' 31 30 import {Outlet as PortalOutlet} from '#/components/Portal' 32 31 import * as Toast from '#/components/Toast' 33 - import {Span, Text} from '#/components/Typography' 32 + import {Text} from '#/components/Typography' 34 33 import {BottomSheetOutlet} from '#/../modules/bottom-sheet' 35 34 import {useAgeAssurance} from '#/ageAssurance' 36 35 import {useAgeAssuranceDataContext} from '#/ageAssurance/data' ··· 54 53 const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed() 55 54 const {logoutCurrentAccount} = useSessionApi() 56 55 const createSupportLink = useCreateSupportLink() 57 - 58 - const {currentAccount} = useSession() 59 - const isUsingAppPassword = isAppPassword(currentAccount?.accessJwt || '') 60 56 61 57 const aa = useAgeAssurance() 62 58 const isBlocked = aa.state.status === aa.Status.Blocked ··· 89 85 logoutCurrentAccount('AgeAssuranceNoAccessScreen') 90 86 }, [logoutCurrentAccount]) 91 87 92 - const birthdateUpdateText = canUpdateBirthday ? ( 93 - <Text style={[textStyles]}> 88 + const orgAdmonition = ( 89 + <Admonition type="tip"> 94 90 <Trans> 95 - If you believe your birthdate is incorrect, you can update it by{' '} 96 - <SimpleInlineLinkText 97 - label={_(msg`Click here to update your birthdate`)} 98 - style={[textStyles]} 99 - {...createStaticClick(() => { 100 - logger.metric('ageAssurance:noAccessScreen:openBirthdateDialog', {}) 101 - birthdateControl.open() 102 - })}> 103 - clicking here 104 - </SimpleInlineLinkText> 105 - . 91 + For organizational accounts, use the birthdate of the person who is 92 + responsible for the account. 106 93 </Trans> 107 - </Text> 94 + </Admonition> 95 + ) 96 + 97 + const birthdateUpdateText = canUpdateBirthday ? ( 98 + <> 99 + <Text style={[textStyles]}> 100 + <Trans> 101 + If you believe your birthdate is incorrect, you can update it by{' '} 102 + <SimpleInlineLinkText 103 + label={_(msg`Click here to update your birthdate`)} 104 + style={[textStyles]} 105 + {...createStaticClick(() => { 106 + logger.metric( 107 + 'ageAssurance:noAccessScreen:openBirthdateDialog', 108 + {}, 109 + ) 110 + birthdateControl.open() 111 + })}> 112 + clicking here 113 + </SimpleInlineLinkText> 114 + . 115 + </Trans> 116 + </Text> 117 + 118 + {orgAdmonition} 119 + </> 108 120 ) : ( 109 121 <Text style={[textStyles]}> 110 122 <Trans> ··· 211 223 </ButtonText> 212 224 </Button> 213 225 214 - {isUsingAppPassword && ( 215 - <Admonition type="info"> 216 - <Trans> 217 - Hmm, it looks like you're logged in with an{' '} 218 - <Span style={[a.italic]}>App Password</Span>. To set your 219 - birthdate, you'll need to log in with your main account 220 - password, or ask whomever controls this account to do so. 221 - </Trans> 222 - </Admonition> 223 - )} 226 + {orgAdmonition} 224 227 </View> 225 228 )} 226 229
+305 -74
src/components/FeedInterstitials.tsx
··· 1 1 import React, {useCallback, useEffect, useRef} from 'react' 2 2 import {ScrollView, View} from 'react-native' 3 + import Animated, {LinearTransition} from 'react-native-reanimated' 3 4 import {type AppBskyFeedDefs, AtUri} from '@atproto/api' 4 5 import {msg, Trans} from '@lingui/macro' 5 6 import {useLingui} from '@lingui/react' 6 7 import {useNavigation} from '@react-navigation/native' 7 8 8 9 import {type NavigationProp} from '#/lib/routes/types' 9 - import {logEvent} from '#/lib/statsig/statsig' 10 + import {logEvent, useGate} from '#/lib/statsig/statsig' 10 11 import {logger} from '#/logger' 11 12 import {type MetricEvents} from '#/logger/metrics' 12 13 import {isIOS} from '#/platform/detection' ··· 15 16 import {useGetPopularFeedsQuery} from '#/state/queries/feed' 16 17 import {type FeedDescriptor} from '#/state/queries/post-feed' 17 18 import {useProfilesQuery} from '#/state/queries/profile' 18 - import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' 19 + import { 20 + useSuggestedFollowsByActorQuery, 21 + useSuggestedFollowsQuery, 22 + } from '#/state/queries/suggested-follows' 19 23 import {useSession} from '#/state/session' 20 24 import * as userActionHistory from '#/state/userActionHistory' 21 25 import {type SeenPost} from '#/state/userActionHistory' ··· 32 36 import * as FeedCard from '#/components/FeedCard' 33 37 import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/icons/Arrow' 34 38 import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag' 39 + import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 35 40 import {InlineLinkText} from '#/components/Link' 36 41 import * as ProfileCard from '#/components/ProfileCard' 37 42 import {Text} from '#/components/Typography' 38 43 import type * as bsky from '#/types/bsky' 39 44 import {FollowDialogWithoutGuide} from './ProgressGuide/FollowDialog' 40 45 import {ProgressGuideList} from './ProgressGuide/List' 46 + 47 + const DISMISS_ANIMATION_DURATION = 200 41 48 42 49 const MOBILE_CARD_WIDTH = 165 43 50 const FINAL_CARD_WIDTH = 120 ··· 203 210 } 204 211 205 212 export function SuggestedFollowsProfile({did}: {did: string}) { 213 + const {gtMobile} = useBreakpoints() 214 + const moderationOpts = useModerationOpts() 215 + const maxLength = gtMobile ? 4 : 6 206 216 const { 207 217 isLoading: isSuggestionsLoading, 208 218 data, ··· 210 220 } = useSuggestedFollowsByActorQuery({ 211 221 did, 212 222 }) 223 + const { 224 + data: moreSuggestions, 225 + fetchNextPage, 226 + hasNextPage, 227 + isFetchingNextPage, 228 + } = useSuggestedFollowsQuery({limit: 25}) 229 + 230 + const [dismissedDids, setDismissedDids] = React.useState<Set<string>>( 231 + new Set(), 232 + ) 233 + const [dismissingDids, setDismissingDids] = React.useState<Set<string>>( 234 + new Set(), 235 + ) 236 + 237 + const onDismiss = React.useCallback((dismissedDid: string) => { 238 + // Start the fade animation 239 + setDismissingDids(prev => new Set(prev).add(dismissedDid)) 240 + // After animation completes, actually remove from list 241 + setTimeout(() => { 242 + setDismissedDids(prev => new Set(prev).add(dismissedDid)) 243 + setDismissingDids(prev => { 244 + const next = new Set(prev) 245 + next.delete(dismissedDid) 246 + return next 247 + }) 248 + }, DISMISS_ANIMATION_DURATION) 249 + }, []) 250 + 251 + // Combine profiles from the actor-specific query with fallback suggestions 252 + const allProfiles = React.useMemo(() => { 253 + const actorProfiles = data?.suggestions ?? [] 254 + const fallbackProfiles = 255 + moreSuggestions?.pages.flatMap(page => page.actors) ?? [] 256 + 257 + // Dedupe by did, preferring actor-specific profiles 258 + const seen = new Set<string>() 259 + const combined: bsky.profile.AnyProfileView[] = [] 260 + 261 + for (const profile of actorProfiles) { 262 + if (!seen.has(profile.did)) { 263 + seen.add(profile.did) 264 + combined.push(profile) 265 + } 266 + } 267 + 268 + for (const profile of fallbackProfiles) { 269 + if (!seen.has(profile.did) && profile.did !== did) { 270 + seen.add(profile.did) 271 + combined.push(profile) 272 + } 273 + } 274 + 275 + return combined 276 + }, [data?.suggestions, moreSuggestions?.pages, did]) 277 + 278 + const filteredProfiles = React.useMemo(() => { 279 + return allProfiles.filter(p => !dismissedDids.has(p.did)) 280 + }, [allProfiles, dismissedDids]) 281 + 282 + // Fetch more when running low 283 + React.useEffect(() => { 284 + if ( 285 + moderationOpts && 286 + filteredProfiles.length < maxLength && 287 + hasNextPage && 288 + !isFetchingNextPage 289 + ) { 290 + fetchNextPage() 291 + } 292 + }, [ 293 + filteredProfiles.length, 294 + maxLength, 295 + hasNextPage, 296 + isFetchingNextPage, 297 + fetchNextPage, 298 + moderationOpts, 299 + ]) 300 + 213 301 return ( 214 302 <ProfileGrid 215 303 isSuggestionsLoading={isSuggestionsLoading} 216 - profiles={data?.suggestions ?? []} 304 + profiles={filteredProfiles} 305 + totalProfileCount={allProfiles.length} 217 306 recId={data?.recId} 218 307 error={error} 219 308 viewContext="profile" 309 + onDismiss={onDismiss} 310 + dismissingDids={dismissingDids} 220 311 /> 221 312 ) 222 313 } 223 314 224 315 export function SuggestedFollowsHome() { 316 + const {gtMobile} = useBreakpoints() 317 + const moderationOpts = useModerationOpts() 318 + const maxLength = gtMobile ? 4 : 6 225 319 const { 226 320 isLoading: isSuggestionsLoading, 227 - profiles, 228 - error, 321 + profiles: experimentalProfiles, 322 + error: experimentalError, 229 323 } = useExperimentalSuggestedUsersQuery() 324 + const { 325 + data: moreSuggestions, 326 + fetchNextPage, 327 + hasNextPage, 328 + isFetchingNextPage, 329 + error: suggestionsError, 330 + } = useSuggestedFollowsQuery({limit: 25}) 331 + 332 + const [dismissedDids, setDismissedDids] = React.useState<Set<string>>( 333 + new Set(), 334 + ) 335 + const [dismissingDids, setDismissingDids] = React.useState<Set<string>>( 336 + new Set(), 337 + ) 338 + 339 + const onDismiss = React.useCallback((did: string) => { 340 + // Start the fade animation 341 + setDismissingDids(prev => new Set(prev).add(did)) 342 + // After animation completes, actually remove from list 343 + setTimeout(() => { 344 + setDismissedDids(prev => new Set(prev).add(did)) 345 + setDismissingDids(prev => { 346 + const next = new Set(prev) 347 + next.delete(did) 348 + return next 349 + }) 350 + }, DISMISS_ANIMATION_DURATION) 351 + }, []) 352 + 353 + // Combine profiles from experimental query with paginated suggestions 354 + const allProfiles = React.useMemo(() => { 355 + const fallbackProfiles = 356 + moreSuggestions?.pages.flatMap(page => page.actors) ?? [] 357 + 358 + // Dedupe by did, preferring experimental profiles 359 + const seen = new Set<string>() 360 + const combined: bsky.profile.AnyProfileView[] = [] 361 + 362 + for (const profile of experimentalProfiles) { 363 + if (!seen.has(profile.did)) { 364 + seen.add(profile.did) 365 + combined.push(profile) 366 + } 367 + } 368 + 369 + for (const profile of fallbackProfiles) { 370 + if (!seen.has(profile.did)) { 371 + seen.add(profile.did) 372 + combined.push(profile) 373 + } 374 + } 375 + 376 + return combined 377 + }, [experimentalProfiles, moreSuggestions?.pages]) 378 + 379 + const filteredProfiles = React.useMemo(() => { 380 + return allProfiles.filter(p => !dismissedDids.has(p.did)) 381 + }, [allProfiles, dismissedDids]) 382 + 383 + // Fetch more when running low 384 + React.useEffect(() => { 385 + if ( 386 + moderationOpts && 387 + filteredProfiles.length < maxLength && 388 + hasNextPage && 389 + !isFetchingNextPage 390 + ) { 391 + fetchNextPage() 392 + } 393 + }, [ 394 + filteredProfiles.length, 395 + maxLength, 396 + hasNextPage, 397 + isFetchingNextPage, 398 + fetchNextPage, 399 + moderationOpts, 400 + ]) 401 + 230 402 return ( 231 403 <ProfileGrid 232 404 isSuggestionsLoading={isSuggestionsLoading} 233 - profiles={profiles} 234 - error={error} 405 + profiles={filteredProfiles} 406 + totalProfileCount={allProfiles.length} 407 + error={experimentalError || suggestionsError} 235 408 viewContext="feed" 409 + onDismiss={onDismiss} 410 + dismissingDids={dismissingDids} 236 411 /> 237 412 ) 238 413 } ··· 241 416 isSuggestionsLoading, 242 417 error, 243 418 profiles, 419 + totalProfileCount, 244 420 recId, 245 421 viewContext = 'feed', 422 + onDismiss, 423 + dismissingDids, 246 424 isVisible = true, 247 425 }: { 248 426 isSuggestionsLoading: boolean 249 427 profiles: bsky.profile.AnyProfileView[] 428 + totalProfileCount?: number 250 429 recId?: number 251 430 error: Error | null 431 + dismissingDids?: Set<string> 252 432 viewContext: 'profile' | 'profileHeader' | 'feed' 433 + onDismiss?: (did: string) => void 253 434 isVisible?: boolean 254 435 }) { 255 436 const t = useTheme() 256 437 const {_} = useLingui() 438 + const gate = useGate() 257 439 const moderationOpts = useModerationOpts() 258 440 const {gtMobile} = useBreakpoints() 259 441 const followDialogControl = useDialogControl() ··· 261 443 const isLoading = isSuggestionsLoading || !moderationOpts 262 444 const isProfileHeaderContext = viewContext === 'profileHeader' 263 445 const isFeedContext = viewContext === 'feed' 446 + const showDismissButton = onDismiss && gate('suggested_users_dismiss') 264 447 265 448 const maxLength = gtMobile ? 3 : isProfileHeaderContext ? 12 : 6 266 449 const minLength = gtMobile ? 3 : 4 ··· 367 550 : error || !profiles.length 368 551 ? null 369 552 : profiles.slice(0, maxLength).map((profile, index) => ( 370 - <ProfileCard.Link 553 + <Animated.View 371 554 key={profile.did} 372 - profile={profile} 373 - onPress={() => { 374 - logEvent('suggestedUser:press', { 375 - logContext: isFeedContext 376 - ? 'InterstitialDiscover' 377 - : 'InterstitialProfile', 378 - recId, 379 - position: index, 380 - suggestedDid: profile.did, 381 - category: null, 382 - }) 383 - }} 555 + layout={LinearTransition.duration(DISMISS_ANIMATION_DURATION)} 384 556 style={[ 385 557 a.flex_1, 386 558 gtMobile && ··· 389 561 a.flex_grow, 390 562 {width: `calc(30% - ${a.gap_md.gap / 2}px)`}, 391 563 ]), 564 + { 565 + opacity: dismissingDids?.has(profile.did) ? 0 : 1, 566 + transitionProperty: 'opacity', 567 + transitionDuration: `${DISMISS_ANIMATION_DURATION}ms`, 568 + }, 392 569 ]}> 393 - {({hovered, pressed}) => ( 394 - <CardOuter 395 - style={[(hovered || pressed) && t.atoms.border_contrast_high]}> 396 - <ProfileCard.Outer> 397 - <View 398 - style={[ 399 - a.flex_col, 400 - a.align_center, 401 - a.gap_sm, 402 - a.pb_sm, 403 - a.mb_auto, 404 - ]}> 405 - <ProfileCard.Avatar 406 - profile={profile} 407 - moderationOpts={moderationOpts} 408 - disabledPreview 409 - size={88} 410 - /> 411 - <View style={[a.flex_col, a.align_center, a.max_w_full]}> 412 - <ProfileCard.Name 570 + <ProfileCard.Link 571 + profile={profile} 572 + onPress={() => { 573 + logEvent('suggestedUser:press', { 574 + logContext: isFeedContext 575 + ? 'InterstitialDiscover' 576 + : 'InterstitialProfile', 577 + recId, 578 + position: index, 579 + suggestedDid: profile.did, 580 + category: null, 581 + }) 582 + }}> 583 + {({hovered, pressed}) => ( 584 + <CardOuter 585 + style={[ 586 + (hovered || pressed) && t.atoms.border_contrast_high, 587 + ]}> 588 + <ProfileCard.Outer> 589 + {showDismissButton && ( 590 + <Button 591 + label={_(msg`Dismiss this suggestion`)} 592 + onPress={e => { 593 + e.preventDefault() 594 + onDismiss!(profile.did) 595 + logEvent('suggestedUser:dismiss', { 596 + logContext: isFeedContext 597 + ? 'InterstitialDiscover' 598 + : 'InterstitialProfile', 599 + position: index, 600 + suggestedDid: profile.did, 601 + recId, 602 + }) 603 + }} 604 + style={[ 605 + a.absolute, 606 + a.z_10, 607 + a.p_xs, 608 + {top: -4, right: -4}, 609 + ]}> 610 + {({ 611 + hovered: dismissHovered, 612 + pressed: dismissPressed, 613 + }) => ( 614 + <X 615 + size="xs" 616 + fill={ 617 + dismissHovered || dismissPressed 618 + ? t.atoms.text.color 619 + : t.atoms.text_contrast_medium.color 620 + } 621 + /> 622 + )} 623 + </Button> 624 + )} 625 + <View 626 + style={[ 627 + a.flex_col, 628 + a.align_center, 629 + a.gap_sm, 630 + a.pb_sm, 631 + a.mb_auto, 632 + ]}> 633 + <ProfileCard.Avatar 413 634 profile={profile} 414 635 moderationOpts={moderationOpts} 636 + disabledPreview 637 + size={88} 415 638 /> 416 - <ProfileCard.Description 417 - profile={profile} 418 - numberOfLines={2} 419 - style={[ 420 - t.atoms.text_contrast_medium, 421 - a.text_center, 422 - a.text_xs, 423 - ]} 424 - /> 639 + <View style={[a.flex_col, a.align_center, a.max_w_full]}> 640 + <ProfileCard.Name 641 + profile={profile} 642 + moderationOpts={moderationOpts} 643 + /> 644 + <ProfileCard.Description 645 + profile={profile} 646 + numberOfLines={2} 647 + style={[ 648 + t.atoms.text_contrast_medium, 649 + a.text_center, 650 + a.text_xs, 651 + ]} 652 + /> 653 + </View> 425 654 </View> 426 - </View> 427 655 428 - <ProfileCard.FollowButton 429 - profile={profile} 430 - moderationOpts={moderationOpts} 431 - logContext="FeedInterstitial" 432 - withIcon={false} 433 - style={[a.rounded_sm]} 434 - onFollow={() => { 435 - logEvent('suggestedUser:follow', { 436 - logContext: isFeedContext 437 - ? 'InterstitialDiscover' 438 - : 'InterstitialProfile', 439 - location: 'Card', 440 - recId, 441 - position: index, 442 - suggestedDid: profile.did, 443 - category: null, 444 - }) 445 - }} 446 - /> 447 - </ProfileCard.Outer> 448 - </CardOuter> 449 - )} 450 - </ProfileCard.Link> 656 + <ProfileCard.FollowButton 657 + profile={profile} 658 + moderationOpts={moderationOpts} 659 + logContext="FeedInterstitial" 660 + withIcon={false} 661 + style={[a.rounded_sm]} 662 + onFollow={() => { 663 + logEvent('suggestedUser:follow', { 664 + logContext: isFeedContext 665 + ? 'InterstitialDiscover' 666 + : 'InterstitialProfile', 667 + location: 'Card', 668 + recId, 669 + position: index, 670 + suggestedDid: profile.did, 671 + category: null, 672 + }) 673 + }} 674 + /> 675 + </ProfileCard.Outer> 676 + </CardOuter> 677 + )} 678 + </ProfileCard.Link> 679 + </Animated.View> 451 680 )) 452 681 453 - if (error || (!isLoading && profiles.length < minLength)) { 682 + // Use totalProfileCount (before dismissals) for minLength check on initial render. 683 + const profileCountForMinCheck = totalProfileCount ?? profiles.length 684 + if (error || (!isLoading && profileCountForMinCheck < minLength)) { 454 685 logger.debug(`Not enough profiles to show suggested follows`) 455 686 return null 456 687 }
+2 -1
src/components/InternationalPhoneCodeSelect.tsx
··· 1 1 import {Fragment, useMemo} from 'react' 2 + import {Text as RNText} from 'react-native' 2 3 import {Image} from 'expo-image' 3 4 import {msg} from '@lingui/macro' 4 5 import {useLingui} from '@lingui/react' ··· 113 114 /> 114 115 ) 115 116 } 116 - return unicodeFlag + ' ' 117 + return <RNText style={[{lineHeight: 21}]}>{unicodeFlag + ' '}</RNText> 117 118 }
+7 -3
src/components/contacts/FindContactsBannerNUX.tsx
··· 6 6 import {useLingui} from '@lingui/react' 7 7 8 8 import {HITSLOP_10} from '#/lib/constants' 9 + import {useGate} from '#/lib/statsig/statsig' 9 10 import {logger} from '#/logger' 10 11 import {isWeb} from '#/platform/detection' 11 12 import {Nux, useNux, useSaveNux} from '#/state/queries/nuxs' ··· 20 21 const t = useTheme() 21 22 const {_} = useLingui() 22 23 const {visible, close} = useInternalState() 23 - const isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation() 24 24 25 - if (!visible || !isFeatureEnabled) return null 25 + if (!visible) return null 26 26 27 27 return ( 28 28 <View style={[a.w_full, a.p_lg, a.border_b, t.atoms.border_contrast_low]}> ··· 88 88 const {nux} = useNux(Nux.FindContactsDismissibleBanner) 89 89 const {mutate: save, variables} = useSaveNux() 90 90 const hidden = !!variables 91 + const isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation() 92 + const gate = useGate() 91 93 92 94 const visible = useMemo(() => { 93 95 if (isWeb) return false 94 96 if (hidden) return false 95 97 if (nux && nux.completed) return false 98 + if (!isFeatureEnabled) return false 99 + if (gate('disable_settings_find_contacts')) return false 96 100 return true 97 - }, [hidden, nux]) 101 + }, [hidden, nux, isFeatureEnabled, gate]) 98 102 99 103 const close = () => { 100 104 save({
+10 -8
src/components/contacts/country-allowlist.ts
··· 18 18 'IT', 19 19 ] satisfies CountryCode[] as string[] 20 20 21 - export function isFindContactsFeatureEnabled(countryCode: string): boolean { 21 + export function isFindContactsFeatureEnabled(countryCode?: string): boolean { 22 + if (IS_DEV) return true 23 + 24 + /* 25 + * This should never happen unless geolocation fails entirely. In that 26 + * case, let the user try, since it should work as long as they have a 27 + * phone number from one of the allow-listed countries. 28 + */ 29 + if (!countryCode) return true 30 + 22 31 return FIND_CONTACTS_FEATURE_COUNTRY_ALLOWLIST.includes( 23 32 countryCode.toUpperCase(), 24 33 ) ··· 26 35 27 36 export function useIsFindContactsFeatureEnabledBasedOnGeolocation() { 28 37 const location = useGeolocation() 29 - 30 - if (IS_DEV) return true 31 - 32 - // they can try, by they'll need a phone number 33 - // from one of the allowlisted countries 34 - if (!location.countryCode) return true 35 - 36 38 return isFindContactsFeatureEnabled(location.countryCode) 37 39 }
+2 -4
src/components/contacts/screens/ViewMatches.tsx
··· 104 104 match => !state.dismissedMatches.includes(match.profile.did), 105 105 ) 106 106 107 - console.log(matches) 108 - 109 107 const followableDids = matches.map(match => match.profile.did) 110 108 const [didFollowAll, setDidFollowAll] = useState(followableDids.length === 0) 111 109 ··· 449 447 const contactName = useMemo(() => { 450 448 if (!contact) return null 451 449 452 - const name = contact.firstName ?? contact.lastName ?? contact.name 450 + const name = contact.name ?? contact.firstName ?? contact.lastName 453 451 if (name) return _(msg`Your contact ${name}`) 454 452 const phone = 455 453 contact.phoneNumbers?.find(p => p.isPrimary) ?? contact.phoneNumbers?.[0] ··· 520 518 const {_} = useLingui() 521 519 const {currentAccount} = useSession() 522 520 523 - const name = contact.firstName ?? contact.lastName ?? contact.name 521 + const name = contact.name ?? contact.firstName ?? contact.lastName 524 522 const phone = 525 523 contact.phoneNumbers?.find(phone => phone.isPrimary) ?? 526 524 contact.phoneNumbers?.[0]
+14 -1
src/components/dialogs/BirthDateSettings.tsx
··· 4 4 import {useLingui} from '@lingui/react' 5 5 6 6 import {useCleanError} from '#/lib/hooks/useCleanError' 7 + import {isAppPassword} from '#/lib/jwt' 7 8 import {getAge, getDateAgo} from '#/lib/strings/time' 8 9 import {logger} from '#/logger' 9 10 import {isIOS, isWeb} from '#/platform/detection' ··· 15 16 usePreferencesQuery, 16 17 type UsePreferencesQueryResponse, 17 18 } from '#/state/queries/preferences' 19 + import {useSession} from '#/state/session' 18 20 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 19 21 import {atoms as a, useTheme, web} from '#/alf' 20 22 import {Admonition} from '#/components/Admonition' ··· 23 25 import {DateField} from '#/components/forms/DateField' 24 26 import {SimpleInlineLinkText} from '#/components/Link' 25 27 import {Loader} from '#/components/Loader' 26 - import {Text} from '#/components/Typography' 28 + import {Span, Text} from '#/components/Typography' 27 29 28 30 export function BirthDateSettingsDialog({ 29 31 control, ··· 34 36 const {_} = useLingui() 35 37 const {isLoading, error, data: preferences} = usePreferencesQuery() 36 38 const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed() 39 + const {currentAccount} = useSession() 40 + const isUsingAppPassword = isAppPassword(currentAccount?.accessJwt || '') 37 41 38 42 return ( 39 43 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> ··· 65 69 } 66 70 style={[a.rounded_sm]} 67 71 /> 72 + ) : isUsingAppPassword ? ( 73 + <Admonition type="info"> 74 + <Trans> 75 + Hmm, it looks like you're logged in with an{' '} 76 + <Span style={[a.italic]}>App Password</Span>. To set your 77 + birthdate, you'll need to log in with your main account 78 + password, or ask whomever controls this account to do so. 79 + </Trans> 80 + </Admonition> 68 81 ) : ( 69 82 <BirthdayInner control={control} preferences={preferences} /> 70 83 )}
+19 -12
src/components/dialogs/nuxs/FindContactsAnnouncement.tsx
··· 6 6 import {useLingui} from '@lingui/react' 7 7 8 8 import {logger} from '#/logger' 9 - import {isWeb} from '#/platform/detection' 9 + import {isNative, isWeb} from '#/platform/detection' 10 10 import {atoms as a, useTheme, web} from '#/alf' 11 11 import {Button, ButtonText} from '#/components/Button' 12 - import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist' 12 + import {isFindContactsFeatureEnabled} from '#/components/contacts/country-allowlist' 13 13 import * as Dialog from '#/components/Dialog' 14 14 import {useNuxDialogContext} from '#/components/dialogs/nuxs' 15 + import { 16 + createIsEnabledCheck, 17 + isExistingUserAsOf, 18 + } from '#/components/dialogs/nuxs/utils' 15 19 import {Text} from '#/components/Typography' 20 + import {IS_E2E} from '#/env' 16 21 import {navigate} from '#/Navigation' 17 22 18 - export function FindContactsAnnouncement() { 19 - const isFeatureEnabled = useIsFindContactsFeatureEnabledBasedOnGeolocation() 23 + export const enabled = createIsEnabledCheck(props => { 24 + return ( 25 + !IS_E2E && 26 + isNative && 27 + isExistingUserAsOf( 28 + '2025-12-16T00:00:00.000Z', 29 + props.currentProfile.createdAt, 30 + ) && 31 + isFindContactsFeatureEnabled(props.geolocation.countryCode) 32 + ) 33 + }) 20 34 21 - if (!isFeatureEnabled) { 22 - return null 23 - } 24 - 25 - return <Inner /> 26 - } 27 - 28 - function Inner() { 35 + export function FindContactsAnnouncement() { 29 36 const t = useTheme() 30 37 const {_} = useLingui() 31 38 const nuxDialogs = useNuxDialogContext()
+17 -21
src/components/dialogs/nuxs/index.tsx
··· 10 10 11 11 import {useGate} from '#/lib/statsig/statsig' 12 12 import {logger} from '#/logger' 13 - import {isNative} from '#/platform/detection' 14 13 import {STALE} from '#/state/queries' 15 14 import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs' 16 15 import { ··· 20 19 import {useProfileQuery} from '#/state/queries/profile' 21 20 import {type SessionAccount, useSession} from '#/state/session' 22 21 import {useOnboardingState} from '#/state/shell' 22 + import { 23 + enabled as isFindContactsAnnouncementEnabled, 24 + FindContactsAnnouncement, 25 + } from '#/components/dialogs/nuxs/FindContactsAnnouncement' 23 26 import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing' 24 - import {ENV} from '#/env' 25 - /* 26 - * NUXs 27 - */ 28 - import {FindContactsAnnouncement} from './FindContactsAnnouncement' 29 - import {isExistingUserAsOf} from './utils' 27 + import {type EnabledCheckProps} from '#/components/dialogs/nuxs/utils' 28 + import {useGeolocation} from '#/geolocation' 30 29 31 30 type Context = { 32 31 activeNux: Nux | undefined ··· 35 34 36 35 const queuedNuxs: { 37 36 id: Nux 38 - enabled?: (props: { 39 - gate: ReturnType<typeof useGate> 40 - currentAccount: SessionAccount 41 - currentProfile: AppBskyActorDefs.ProfileViewDetailed 42 - preferences: UsePreferencesQueryResponse 43 - }) => boolean 37 + enabled?: (props: EnabledCheckProps) => boolean 44 38 }[] = [ 45 39 { 46 40 id: Nux.FindContactsAnnouncement, 47 - enabled: ({currentProfile}) => { 48 - return ( 49 - isNative && 50 - ENV !== 'e2e' && 51 - isExistingUserAsOf('2025-12-16T00:00:00.000Z', currentProfile.createdAt) 52 - ) 53 - }, 41 + enabled: isFindContactsAnnouncementEnabled, 54 42 }, 55 43 ] 56 44 ··· 101 89 preferences: UsePreferencesQueryResponse 102 90 }) { 103 91 const gate = useGate() 92 + const geolocation = useGeolocation() 104 93 const {nuxs} = useNuxs() 105 94 const [snoozed, setSnoozed] = useState(() => { 106 95 return isSnoozed() ··· 143 132 // then check gate (track exposure) 144 133 if ( 145 134 enabled && 146 - !enabled({gate, currentAccount, currentProfile, preferences}) 135 + !enabled({ 136 + gate, 137 + currentAccount, 138 + currentProfile, 139 + preferences, 140 + geolocation, 141 + }) 147 142 ) { 148 143 continue 149 144 } ··· 178 173 currentAccount, 179 174 currentProfile, 180 175 preferences, 176 + geolocation, 181 177 ]) 182 178 183 179 const ctx = useMemo(() => {
+21
src/components/dialogs/nuxs/utils.ts
··· 1 + import {type AppBskyActorDefs} from '@atproto/api' 2 + 3 + import {type useGate} from '#/lib/statsig/statsig' 4 + import {type UsePreferencesQueryResponse} from '#/state/queries/preferences' 5 + import {type SessionAccount} from '#/state/session' 6 + import {type Geolocation} from '#/geolocation' 7 + 8 + export type EnabledCheckProps = { 9 + gate: ReturnType<typeof useGate> 10 + currentAccount: SessionAccount 11 + currentProfile: AppBskyActorDefs.ProfileViewDetailed 12 + preferences: UsePreferencesQueryResponse 13 + geolocation: Geolocation 14 + } 15 + 16 + export function createIsEnabledCheck( 17 + cb: (props: EnabledCheckProps) => boolean, 18 + ) { 19 + return cb 20 + } 21 + 1 22 const ONE_DAY = 1000 * 60 * 60 * 24 2 23 3 24 export function isDaysOld(days: number, createdAt?: string) {
+2
src/lib/statsig/gates.ts
··· 4 4 | 'debug_show_feedcontext' 5 5 | 'debug_subscriptions' 6 6 | 'disable_onboarding_find_contacts' 7 + | 'disable_settings_find_contacts' 7 8 | 'explore_show_suggested_feeds' 8 9 | 'feed_reply_button_open_thread' 9 10 | 'old_postonboarding' ··· 11 12 | 'onboarding_suggested_starterpacks' 12 13 | 'remove_show_latest_button' 13 14 | 'show_composer_prompt' 15 + | 'suggested_users_dismiss' 14 16 | 'test_gate_1' 15 17 | 'test_gate_2'
+201 -178
src/locale/locales/en/messages.po
··· 38 38 msgid "{0, plural, one {# following} other {# following}}" 39 39 msgstr "" 40 40 41 - #: src/components/contacts/screens/ViewMatches.tsx:267 41 + #: src/components/contacts/screens/ViewMatches.tsx:265 42 42 msgid "{0, plural, one {# friend found!} other {# friends found!}}" 43 43 msgstr "" 44 44 ··· 576 576 577 577 #: src/Navigation.tsx:534 578 578 #: src/screens/Settings/AboutSettings.tsx:75 579 - #: src/screens/Settings/Settings.tsx:258 580 - #: src/screens/Settings/Settings.tsx:261 579 + #: src/screens/Settings/Settings.tsx:262 580 + #: src/screens/Settings/Settings.tsx:265 581 581 msgid "About" 582 582 msgstr "" 583 583 ··· 604 604 msgstr "" 605 605 606 606 #: src/screens/Settings/AccessibilitySettings.tsx:44 607 - #: src/screens/Settings/Settings.tsx:234 608 - #: src/screens/Settings/Settings.tsx:237 607 + #: src/screens/Settings/Settings.tsx:238 608 + #: src/screens/Settings/Settings.tsx:241 609 609 msgid "Accessibility" 610 610 msgstr "" 611 611 ··· 616 616 #: src/Navigation.tsx:401 617 617 #: src/screens/Login/LoginForm.tsx:194 618 618 #: src/screens/Settings/AccountSettings.tsx:51 619 - #: src/screens/Settings/Settings.tsx:178 620 - #: src/screens/Settings/Settings.tsx:181 619 + #: src/screens/Settings/Settings.tsx:180 620 + #: src/screens/Settings/Settings.tsx:183 621 621 msgid "Account" 622 622 msgstr "" 623 623 ··· 648 648 msgid "Account Muted by List" 649 649 msgstr "" 650 650 651 - #: src/screens/Settings/Settings.tsx:640 651 + #: src/screens/Settings/Settings.tsx:644 652 652 msgid "Account options" 653 653 msgstr "" 654 654 ··· 656 656 msgid "Account provider" 657 657 msgstr "" 658 658 659 - #: src/screens/Settings/Settings.tsx:676 659 + #: src/screens/Settings/Settings.tsx:680 660 660 msgid "Account removed from quick access" 661 661 msgstr "" 662 662 ··· 741 741 msgid "Add alt text (optional)" 742 742 msgstr "" 743 743 744 - #: src/screens/Settings/Settings.tsx:580 745 - #: src/screens/Settings/Settings.tsx:583 744 + #: src/screens/Settings/Settings.tsx:584 745 + #: src/screens/Settings/Settings.tsx:587 746 746 #: src/view/shell/desktop/LeftNav.tsx:262 747 747 #: src/view/shell/desktop/LeftNav.tsx:266 748 748 msgid "Add another account" ··· 844 844 msgid "Add user to list" 845 845 msgstr "" 846 846 847 - #: src/ageAssurance/components/NoAccessScreen.tsx:210 847 + #: src/ageAssurance/components/NoAccessScreen.tsx:222 848 848 msgid "Add your birthdate" 849 849 msgstr "" 850 850 ··· 914 914 msgid "Age assurance inquiry was submitted" 915 915 msgstr "" 916 916 917 - #: src/ageAssurance/components/NoAccessScreen.tsx:342 917 + #: src/ageAssurance/components/NoAccessScreen.tsx:345 918 918 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:191 919 919 msgid "Age assurance only takes a few minutes" 920 920 msgstr "" ··· 934 934 msgid "All accounts have been followed!" 935 935 msgstr "" 936 936 937 - #: src/components/contacts/screens/ViewMatches.tsx:143 937 + #: src/components/contacts/screens/ViewMatches.tsx:141 938 938 msgid "All friends followed!" 939 939 msgstr "" 940 940 ··· 1070 1070 msgid "An error occurred while generating your starter pack. Want to try again?" 1071 1071 msgstr "" 1072 1072 1073 - #: src/components/contacts/screens/ViewMatches.tsx:242 1073 + #: src/components/contacts/screens/ViewMatches.tsx:240 1074 1074 msgid "An error occurred while hiding suggestion. {0}" 1075 1075 msgstr "" 1076 1076 ··· 1108 1108 msgstr "" 1109 1109 1110 1110 #: src/components/contacts/components/HeroImage.tsx:28 1111 - #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:71 1111 + #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:78 1112 1112 msgid "An illustration depicting user avatars flowing from a contact book into the Bluesky app" 1113 1113 msgstr "" 1114 1114 ··· 1272 1272 1273 1273 #: src/Navigation.tsx:393 1274 1274 #: src/screens/Settings/AppearanceSettings.tsx:73 1275 - #: src/screens/Settings/Settings.tsx:226 1276 - #: src/screens/Settings/Settings.tsx:229 1275 + #: src/screens/Settings/Settings.tsx:230 1276 + #: src/screens/Settings/Settings.tsx:233 1277 1277 msgid "Appearance" 1278 1278 msgstr "" 1279 1279 ··· 1282 1282 msgid "Apply default recommended feeds" 1283 1283 msgstr "" 1284 1284 1285 - #: src/screens/Settings/Settings.tsx:512 1286 - #: src/screens/Settings/Settings.tsx:514 1285 + #: src/screens/Settings/Settings.tsx:516 1286 + #: src/screens/Settings/Settings.tsx:518 1287 1287 msgid "Apply Pull Request" 1288 1288 msgstr "" 1289 1289 ··· 1448 1448 msgid "Begin the age assurance process by completing the fields below." 1449 1449 msgstr "" 1450 1450 1451 - #: src/components/dialogs/BirthDateSettings.tsx:149 1451 + #: src/components/dialogs/BirthDateSettings.tsx:162 1452 1452 msgid "Birthdate" 1453 1453 msgstr "" 1454 1454 ··· 1585 1585 msgid "Bluesky is more fun with friends" 1586 1586 msgstr "" 1587 1587 1588 - #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:99 1588 + #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:106 1589 1589 msgid "Bluesky is more fun with friends! Import your contacts to see who’s already here." 1590 1590 msgstr "" 1591 1591 1592 - #: src/components/contacts/screens/ViewMatches.tsx:314 1592 + #: src/components/contacts/screens/ViewMatches.tsx:312 1593 1593 msgid "Bluesky is more fun with friends. Do you want to invite some of yours? <0/>" 1594 1594 msgstr "" 1595 1595 ··· 1637 1637 msgid "Breaking site rules" 1638 1638 msgstr "" 1639 1639 1640 - #: src/view/com/feeds/ProfileFeedgens.tsx:158 1641 - #: src/view/com/feeds/ProfileFeedgens.tsx:159 1640 + #: src/view/com/feeds/ProfileFeedgens.tsx:167 1641 + #: src/view/com/feeds/ProfileFeedgens.tsx:168 1642 1642 msgid "Browse custom feeds" 1643 1643 msgstr "" 1644 1644 1645 - #: src/components/FeedInterstitials.tsx:547 1645 + #: src/components/FeedInterstitials.tsx:778 1646 1646 msgid "Browse more accounts" 1647 1647 msgstr "" 1648 1648 1649 - #: src/components/FeedInterstitials.tsx:676 1649 + #: src/components/FeedInterstitials.tsx:907 1650 1650 msgid "Browse more feeds on the Explore page" 1651 1651 msgstr "" 1652 1652 1653 - #: src/components/FeedInterstitials.tsx:657 1654 - #: src/components/FeedInterstitials.tsx:660 1653 + #: src/components/FeedInterstitials.tsx:888 1654 + #: src/components/FeedInterstitials.tsx:891 1655 1655 msgid "Browse more suggestions" 1656 1656 msgstr "" 1657 1657 1658 - #: src/components/FeedInterstitials.tsx:685 1658 + #: src/components/FeedInterstitials.tsx:916 1659 1659 msgid "Browse more suggestions on the Explore page" 1660 1660 msgstr "" 1661 1661 ··· 1761 1761 #: src/screens/Settings/components/ChangeHandleDialog.tsx:85 1762 1762 #: src/screens/Settings/components/ChangePasswordDialog.tsx:247 1763 1763 #: src/screens/Settings/components/ChangePasswordDialog.tsx:253 1764 - #: src/screens/Settings/Settings.tsx:303 1764 + #: src/screens/Settings/Settings.tsx:307 1765 1765 #: src/screens/Takendown.tsx:107 1766 1766 #: src/screens/Takendown.tsx:110 1767 1767 #: src/view/com/composer/Composer.tsx:1050 ··· 1997 1997 msgid "Choose your username" 1998 1998 msgstr "" 1999 1999 2000 - #: src/screens/Settings/Settings.tsx:504 2000 + #: src/screens/Settings/Settings.tsx:508 2001 2001 msgid "Clear all storage data" 2002 2002 msgstr "" 2003 2003 2004 - #: src/screens/Settings/Settings.tsx:506 2004 + #: src/screens/Settings/Settings.tsx:510 2005 2005 msgid "Clear all storage data (restart after this)" 2006 2006 msgstr "" 2007 2007 ··· 2022 2022 msgid "click here" 2023 2023 msgstr "" 2024 2024 2025 - #: src/ageAssurance/components/NoAccessScreen.tsx:114 2025 + #: src/ageAssurance/components/NoAccessScreen.tsx:126 2026 2026 msgid "Click here to contact our support team" 2027 2027 msgstr "" 2028 2028 2029 - #: src/ageAssurance/components/NoAccessScreen.tsx:233 2029 + #: src/ageAssurance/components/NoAccessScreen.tsx:236 2030 2030 msgid "Click here to log out" 2031 2031 msgstr "" 2032 2032 ··· 2034 2034 msgid "Click here to restart the verification process." 2035 2035 msgstr "" 2036 2036 2037 - #: src/ageAssurance/components/NoAccessScreen.tsx:97 2038 - #: src/ageAssurance/components/NoAccessScreen.tsx:207 2037 + #: src/ageAssurance/components/NoAccessScreen.tsx:103 2038 + #: src/ageAssurance/components/NoAccessScreen.tsx:219 2039 2039 msgid "Click here to update your birthdate" 2040 2040 msgstr "" 2041 2041 ··· 2121 2121 msgid "Close dialog" 2122 2122 msgstr "" 2123 2123 2124 - #: src/view/shell/index.web.tsx:111 2124 + #: src/view/shell/index.web.tsx:113 2125 2125 msgid "Close drawer menu" 2126 2126 msgstr "" 2127 2127 ··· 2252 2252 msgid "Confirm delete account" 2253 2253 msgstr "" 2254 2254 2255 - #: src/ageAssurance/components/NoAccessScreen.tsx:356 2255 + #: src/ageAssurance/components/NoAccessScreen.tsx:359 2256 2256 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:87 2257 2257 #: src/components/dialogs/DeviceLocationRequestDialog.tsx:40 2258 2258 #: src/components/dialogs/DeviceLocationRequestDialog.tsx:105 ··· 2279 2279 msgid "Connection issue" 2280 2280 msgstr "" 2281 2281 2282 - #: src/ageAssurance/components/NoAccessScreen.tsx:293 2282 + #: src/ageAssurance/components/NoAccessScreen.tsx:296 2283 2283 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:130 2284 2284 #: src/components/ageAssurance/AgeAssuranceAppealDialog.tsx:29 2285 2285 msgid "Contact our moderation team" ··· 2312 2312 msgid "Content & Media" 2313 2313 msgstr "" 2314 2314 2315 - #: src/screens/Settings/Settings.tsx:208 2316 - #: src/screens/Settings/Settings.tsx:211 2315 + #: src/screens/Settings/Settings.tsx:210 2316 + #: src/screens/Settings/Settings.tsx:213 2317 2317 msgid "Content and media" 2318 2318 msgstr "" 2319 2319 ··· 2581 2581 msgid "Could not upload contacts. You need to re-verify your phone number to proceed" 2582 2582 msgstr "" 2583 2583 2584 - #: src/components/InternationalPhoneCodeSelect.tsx:79 2584 + #: src/components/InternationalPhoneCodeSelect.tsx:80 2585 2585 msgid "Country code" 2586 2586 msgstr "" 2587 2587 ··· 2593 2593 msgid "Create" 2594 2594 msgstr "" 2595 2595 2596 - #: src/view/com/lists/ProfileLists.tsx:159 2597 - #: src/view/com/lists/ProfileLists.tsx:160 2596 + #: src/view/com/lists/ProfileLists.tsx:166 2597 + #: src/view/com/lists/ProfileLists.tsx:167 2598 2598 msgid "Create a list" 2599 2599 msgstr "" 2600 2600 ··· 2608 2608 msgid "Create a starter pack" 2609 2609 msgstr "" 2610 2610 2611 - #: src/view/screens/Profile.tsx:542 2612 - #: src/view/screens/Profile.tsx:543 2611 + #: src/view/screens/Profile.tsx:560 2612 + #: src/view/screens/Profile.tsx:561 2613 2613 msgid "Create a Starter Pack" 2614 2614 msgstr "" 2615 2615 ··· 2741 2741 msgid "Deactivate account" 2742 2742 msgstr "" 2743 2743 2744 - #: src/screens/Settings/Settings.tsx:469 2744 + #: src/screens/Settings/Settings.tsx:473 2745 2745 msgid "Debug Moderation" 2746 2746 msgstr "" 2747 2747 ··· 2794 2794 msgid "Delete chat" 2795 2795 msgstr "" 2796 2796 2797 - #: src/screens/Settings/Settings.tsx:476 2797 + #: src/screens/Settings/Settings.tsx:480 2798 2798 msgid "Delete chat declaration record" 2799 2799 msgstr "" 2800 2800 ··· 2907 2907 msgid "Developer mode enabled" 2908 2908 msgstr "" 2909 2909 2910 - #: src/screens/Settings/Settings.tsx:285 2911 - #: src/screens/Settings/Settings.tsx:288 2910 + #: src/screens/Settings/Settings.tsx:289 2911 + #: src/screens/Settings/Settings.tsx:292 2912 2912 msgid "Developer options" 2913 2913 msgstr "" 2914 2914 ··· 3018 3018 msgid "Dismiss this section" 3019 3019 msgstr "" 3020 3020 3021 + #: src/components/FeedInterstitials.tsx:587 3022 + msgid "Dismiss this suggestion" 3023 + msgstr "" 3024 + 3021 3025 #: src/screens/Settings/AccessibilitySettings.tsx:69 3022 3026 #: src/screens/Settings/AccessibilitySettings.tsx:74 3023 3027 msgid "Display larger alt text badges" ··· 3068 3072 3069 3073 #: src/components/contacts/components/InviteInfo.tsx:72 3070 3074 #: src/components/contacts/components/InviteInfo.tsx:78 3071 - #: src/components/contacts/screens/ViewMatches.tsx:394 3072 - #: src/components/contacts/screens/ViewMatches.tsx:411 3073 - #: src/components/dialogs/BirthDateSettings.tsx:183 3074 - #: src/components/dialogs/BirthDateSettings.tsx:190 3075 + #: src/components/contacts/screens/ViewMatches.tsx:392 3076 + #: src/components/contacts/screens/ViewMatches.tsx:409 3077 + #: src/components/dialogs/BirthDateSettings.tsx:196 3078 + #: src/components/dialogs/BirthDateSettings.tsx:203 3075 3079 #: src/components/dialogs/ServerInput.tsx:240 3076 3080 #: src/components/dialogs/ServerInput.tsx:242 3077 3081 #: src/components/dms/AfterReportDialog.tsx:142 ··· 3445 3449 msgid "Enter the username or email address you used when you created your account" 3446 3450 msgstr "" 3447 3451 3448 - #: src/components/dialogs/BirthDateSettings.tsx:150 3452 + #: src/components/dialogs/BirthDateSettings.tsx:163 3449 3453 msgid "Enter your birthdate" 3450 3454 msgstr "" 3451 3455 ··· 3695 3699 msgid "Failed to follow all suggested accounts, please try again" 3696 3700 msgstr "" 3697 3701 3698 - #: src/components/contacts/screens/ViewMatches.tsx:147 3702 + #: src/components/contacts/screens/ViewMatches.tsx:145 3699 3703 msgid "Failed to follow all your friends, please try again" 3700 3704 msgstr "" 3701 3705 3702 - #: src/components/contacts/screens/ViewMatches.tsx:235 3706 + #: src/components/contacts/screens/ViewMatches.tsx:233 3703 3707 msgid "Failed to hide suggestion, please check your internet connection" 3704 3708 msgstr "" 3705 3709 3706 - #: src/components/contacts/screens/ViewMatches.tsx:580 3710 + #: src/components/contacts/screens/ViewMatches.tsx:578 3707 3711 msgid "Failed to launch SMS app" 3708 3712 msgstr "" 3709 3713 ··· 3991 3995 msgid "Find Friends" 3992 3996 msgstr "" 3993 3997 3994 - #: src/screens/Settings/Settings.tsx:217 3995 - #: src/screens/Settings/Settings.tsx:220 3998 + #: src/screens/Settings/Settings.tsx:221 3999 + #: src/screens/Settings/Settings.tsx:224 3996 4000 msgid "Find friends from contacts" 3997 4001 msgstr "" 3998 4002 ··· 4013 4017 msgid "Find posts, users, and feeds on Bluesky" 4014 4018 msgstr "" 4015 4019 4016 - #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:89 4020 + #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:96 4017 4021 msgid "Find your friends" 4018 4022 msgstr "" 4019 4023 ··· 4088 4092 msgid "Follow account" 4089 4093 msgstr "" 4090 4094 4091 - #: src/components/contacts/screens/ViewMatches.tsx:275 4092 - #: src/components/contacts/screens/ViewMatches.tsx:290 4095 + #: src/components/contacts/screens/ViewMatches.tsx:273 4096 + #: src/components/contacts/screens/ViewMatches.tsx:288 4093 4097 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:265 4094 4098 #: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:156 4095 4099 #: src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx:163 ··· 4203 4207 4204 4208 #: src/lib/interests.ts:61 4205 4209 msgid "Food" 4210 + msgstr "" 4211 + 4212 + #: src/ageAssurance/components/NoAccessScreen.tsx:90 4213 + msgid "For organizational accounts, use the birthdate of the person who is responsible for the account." 4206 4214 msgstr "" 4207 4215 4208 4216 #: src/view/com/modals/DeleteAccount.tsx:125 ··· 4527 4535 msgid "Held by Bluesky for 7 days to prevent abuse, then deleted" 4528 4536 msgstr "" 4529 4537 4530 - #: src/screens/Settings/Settings.tsx:250 4531 4538 #: src/screens/Settings/Settings.tsx:254 4539 + #: src/screens/Settings/Settings.tsx:258 4532 4540 #: src/view/shell/desktop/RightNav.tsx:123 4533 4541 #: src/view/shell/desktop/RightNav.tsx:124 4534 4542 #: src/view/shell/Drawer.tsx:381 ··· 4548 4556 msgid "Hey there 👋" 4549 4557 msgstr "" 4550 4558 4551 - #: src/ageAssurance/components/NoAccessScreen.tsx:158 4559 + #: src/ageAssurance/components/NoAccessScreen.tsx:170 4552 4560 msgid "Hey there!" 4553 4561 msgstr "" 4554 4562 4555 - #: src/ageAssurance/components/NoAccessScreen.tsx:189 4563 + #: src/ageAssurance/components/NoAccessScreen.tsx:201 4556 4564 msgid "Hi there!" 4557 4565 msgstr "" 4558 4566 ··· 4652 4660 msgid "Hides the content" 4653 4661 msgstr "" 4654 4662 4655 - #: src/ageAssurance/components/NoAccessScreen.tsx:216 4663 + #: src/components/dialogs/BirthDateSettings.tsx:74 4656 4664 msgid "Hmm, it looks like you're logged in with an <0>App Password</0>. To set your birthdate, you'll need to log in with your main account password, or ask whomever controls this account to do so." 4657 4665 msgstr "" 4658 4666 ··· 4745 4753 msgid "I understand" 4746 4754 msgstr "" 4747 4755 4748 - #: src/components/contacts/screens/ViewMatches.tsx:576 4756 + #: src/components/contacts/screens/ViewMatches.tsx:574 4749 4757 msgid "I'm on Bluesky as {0} - come find me! https://bsky.app/download" 4750 4758 msgstr "" 4751 4759 ··· 4757 4765 msgid "If you are not yet an adult according to the laws of your country, your parent or legal guardian must read these Terms on your behalf." 4758 4766 msgstr "" 4759 4767 4760 - #: src/ageAssurance/components/NoAccessScreen.tsx:110 4768 + #: src/ageAssurance/components/NoAccessScreen.tsx:122 4761 4769 msgid "If you believe your birthdate is incorrect, please <0>contact our support team</0>." 4762 4770 msgstr "" 4763 4771 4764 - #: src/ageAssurance/components/NoAccessScreen.tsx:94 4772 + #: src/ageAssurance/components/NoAccessScreen.tsx:100 4765 4773 msgid "If you believe your birthdate is incorrect, you can update it by <0>clicking here</0>." 4766 4774 msgstr "" 4767 4775 ··· 4835 4843 msgid "Import contacts" 4836 4844 msgstr "" 4837 4845 4838 - #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:107 4839 - #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:118 4846 + #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:114 4847 + #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:125 4840 4848 msgid "Import Contacts" 4841 4849 msgstr "" 4842 4850 ··· 4845 4853 msgid "Import contacts to find your friends" 4846 4854 msgstr "" 4847 4855 4848 - #: src/ageAssurance/components/NoAccessScreen.tsx:192 4856 + #: src/ageAssurance/components/NoAccessScreen.tsx:204 4849 4857 msgid "In order to provide an age-appropriate experience, we need to know your birthdate. This is a one-time thing, and your data will be kept private." 4850 4858 msgstr "" 4851 4859 ··· 4918 4926 msgid "Introducing activity notifications" 4919 4927 msgstr "" 4920 4928 4921 - #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:48 4929 + #: src/components/dialogs/nuxs/FindContactsAnnouncement.tsx:55 4922 4930 msgid "Introducing finding friends via contacts" 4923 4931 msgstr "" 4924 4932 ··· 4953 4961 msgid "Invalid Verification Code" 4954 4962 msgstr "" 4955 4963 4956 - #: src/components/contacts/screens/ViewMatches.tsx:585 4964 + #: src/components/contacts/screens/ViewMatches.tsx:583 4957 4965 msgid "Invite" 4958 4966 msgstr "" 4959 4967 4960 - #: src/components/contacts/screens/ViewMatches.tsx:565 4968 + #: src/components/contacts/screens/ViewMatches.tsx:563 4961 4969 msgid "Invite {name} to join Bluesky" 4962 4970 msgstr "" 4963 4971 ··· 4974 4982 msgid "Invite Friends" 4975 4983 msgstr "" 4976 4984 4977 - #: src/components/contacts/screens/ViewMatches.tsx:300 4985 + #: src/components/contacts/screens/ViewMatches.tsx:298 4978 4986 msgid "Invite friends <0/>" 4979 4987 msgstr "" 4980 4988 ··· 4990 4998 msgid "Invites, but personal" 4991 4999 msgstr "" 4992 5000 4993 - #: src/ageAssurance/components/NoAccessScreen.tsx:353 5001 + #: src/ageAssurance/components/NoAccessScreen.tsx:356 4994 5002 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:84 4995 5003 msgid "Is your location not accurate? <0>Tap here to confirm your location.</0>" 4996 5004 msgstr "" ··· 5078 5086 msgstr "" 5079 5087 5080 5088 #: src/screens/Settings/LanguageSettings.tsx:78 5081 - #: src/screens/Settings/Settings.tsx:242 5082 - #: src/screens/Settings/Settings.tsx:245 5089 + #: src/screens/Settings/Settings.tsx:246 5090 + #: src/screens/Settings/Settings.tsx:249 5083 5091 msgid "Languages" 5084 5092 msgstr "" 5085 5093 ··· 5087 5095 msgid "Larger" 5088 5096 msgstr "" 5089 5097 5090 - #: src/ageAssurance/components/NoAccessScreen.tsx:336 5098 + #: src/ageAssurance/components/NoAccessScreen.tsx:339 5091 5099 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:185 5092 5100 msgid "Last initiated {timeAgo} ago" 5093 5101 msgstr "" 5094 5102 5095 - #: src/ageAssurance/components/NoAccessScreen.tsx:334 5103 + #: src/ageAssurance/components/NoAccessScreen.tsx:337 5096 5104 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:183 5097 5105 msgid "Last initiated just now" 5098 5106 msgstr "" ··· 5401 5409 msgstr "" 5402 5410 5403 5411 #: src/view/com/lists/MyLists.tsx:72 5404 - #: src/view/com/lists/ProfileLists.tsx:155 5405 5412 msgid "Lists allow you to see content from your favorite people." 5406 5413 msgstr "" 5407 5414 ··· 5628 5635 5629 5636 #: src/Navigation.tsx:179 5630 5637 #: src/screens/Moderation/index.tsx:99 5631 - #: src/screens/Settings/Settings.tsx:192 5632 - #: src/screens/Settings/Settings.tsx:195 5638 + #: src/screens/Settings/Settings.tsx:194 5639 + #: src/screens/Settings/Settings.tsx:197 5633 5640 msgid "Moderation" 5634 5641 msgstr "" 5635 5642 ··· 5818 5825 msgid "Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them." 5819 5826 msgstr "" 5820 5827 5821 - #: src/components/dialogs/BirthDateSettings.tsx:43 5822 5828 #: src/components/dialogs/BirthDateSettings.tsx:47 5829 + #: src/components/dialogs/BirthDateSettings.tsx:51 5823 5830 msgid "My Birthdate" 5824 5831 msgstr "" 5825 5832 ··· 5932 5939 #: src/screens/ProfileList/index.tsx:284 5933 5940 #: src/view/screens/Feeds.tsx:552 5934 5941 #: src/view/screens/Notifications.tsx:167 5935 - #: src/view/screens/Profile.tsx:571 5942 + #: src/view/screens/Profile.tsx:591 5936 5943 msgid "New post" 5937 5944 msgstr "" 5938 5945 ··· 5974 5981 msgid "News" 5975 5982 msgstr "" 5976 5983 5977 - #: src/components/contacts/screens/ViewMatches.tsx:394 5978 - #: src/components/contacts/screens/ViewMatches.tsx:409 5984 + #: src/components/contacts/screens/ViewMatches.tsx:392 5985 + #: src/components/contacts/screens/ViewMatches.tsx:407 5979 5986 #: src/screens/Login/ForgotPasswordForm.tsx:137 5980 5987 #: src/screens/Login/ForgotPasswordForm.tsx:143 5981 5988 #: src/screens/Login/LoginForm.tsx:346 ··· 6006 6013 msgid "No app passwords yet" 6007 6014 msgstr "" 6008 6015 6009 - #: src/components/contacts/screens/ViewMatches.tsx:679 6016 + #: src/components/contacts/screens/ViewMatches.tsx:677 6010 6017 msgid "No contacts found" 6011 6018 msgstr "" 6012 6019 6013 - #: src/components/contacts/screens/ViewMatches.tsx:657 6020 + #: src/components/contacts/screens/ViewMatches.tsx:655 6014 6021 msgid "No contacts with the name “{query}” found" 6022 + msgstr "" 6023 + 6024 + #: src/view/com/feeds/ProfileFeedgens.tsx:161 6025 + msgid "No custom feeds yet" 6015 6026 msgstr "" 6016 6027 6017 6028 #: src/screens/Settings/components/ChangeHandleDialog.tsx:408 ··· 6041 6052 6042 6053 #: src/components/LikedByList.tsx:84 6043 6054 #: src/view/com/post-thread/PostLikedBy.tsx:84 6044 - #: src/view/screens/Profile.tsx:511 6055 + #: src/view/screens/Profile.tsx:523 6045 6056 msgid "No likes yet" 6057 + msgstr "" 6058 + 6059 + #: src/view/com/lists/ProfileLists.tsx:160 6060 + msgid "No lists" 6046 6061 msgstr "" 6047 6062 6048 6063 #: src/components/ProfileCard.tsx:531 ··· 6051 6066 msgid "No longer following {0}" 6052 6067 msgstr "" 6053 6068 6054 - #: src/view/screens/Profile.tsx:467 6069 + #: src/view/screens/Profile.tsx:471 6055 6070 msgid "No media yet" 6056 6071 msgstr "" 6057 6072 ··· 6063 6078 msgid "No more doomscrolling junk-filled algorithms. Find feeds that work for you, not against you." 6064 6079 msgstr "" 6065 6080 6066 - #: src/components/contacts/screens/ViewMatches.tsx:561 6081 + #: src/components/contacts/screens/ViewMatches.tsx:559 6067 6082 msgid "No name" 6068 6083 msgstr "" 6069 6084 ··· 6096 6111 msgid "No quotes yet" 6097 6112 msgstr "" 6098 6113 6099 - #: src/view/screens/Profile.tsx:452 6114 + #: src/view/screens/Profile.tsx:456 6100 6115 msgid "No replies yet" 6101 6116 msgstr "" 6102 6117 ··· 6139 6154 msgid "No search results found for \"{search}\"." 6140 6155 msgstr "" 6141 6156 6157 + #: src/view/screens/Profile.tsx:555 6158 + msgid "No Starter Packs yet" 6159 + msgstr "" 6160 + 6142 6161 #: src/components/dialogs/EmbedConsent.tsx:104 6143 6162 #: src/components/dialogs/EmbedConsent.tsx:111 6144 6163 msgid "No thanks" 6145 6164 msgstr "" 6146 6165 6147 - #: src/view/screens/Profile.tsx:489 6166 + #: src/view/screens/Profile.tsx:497 6148 6167 msgid "No video posts yet" 6149 6168 msgstr "" 6150 6169 ··· 6244 6263 #: src/screens/Settings/NotificationSettings/ReplyNotificationSettings.tsx:30 6245 6264 #: src/screens/Settings/NotificationSettings/RepostNotificationSettings.tsx:30 6246 6265 #: src/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings.tsx:30 6247 - #: src/screens/Settings/Settings.tsx:200 6248 - #: src/screens/Settings/Settings.tsx:203 6266 + #: src/screens/Settings/Settings.tsx:202 6267 + #: src/screens/Settings/Settings.tsx:205 6249 6268 #: src/view/screens/Notifications.tsx:130 6250 6269 #: src/view/shell/bottom-bar/BottomBar.tsx:252 6251 6270 #: src/view/shell/desktop/LeftNav.tsx:709 ··· 6310 6329 msgid "on<0><1/><2><3/></2></0>" 6311 6330 msgstr "" 6312 6331 6313 - #: src/screens/Settings/Settings.tsx:402 6332 + #: src/screens/Settings/Settings.tsx:406 6314 6333 msgid "Onboarding reset" 6315 6334 msgstr "" 6316 6335 ··· 6412 6431 msgid "Open message options" 6413 6432 msgstr "" 6414 6433 6415 - #: src/screens/Settings/Settings.tsx:467 6434 + #: src/screens/Settings/Settings.tsx:471 6416 6435 msgid "Open moderation debug page" 6417 6436 msgstr "" 6418 6437 ··· 6441 6460 msgid "Open starter pack menu" 6442 6461 msgstr "" 6443 6462 6444 - #: src/screens/Settings/Settings.tsx:460 6445 - #: src/screens/Settings/Settings.tsx:474 6463 + #: src/screens/Settings/Settings.tsx:464 6464 + #: src/screens/Settings/Settings.tsx:478 6446 6465 msgid "Open storybook page" 6447 6466 msgstr "" 6448 6467 6449 - #: src/screens/Settings/Settings.tsx:453 6468 + #: src/screens/Settings/Settings.tsx:457 6450 6469 msgid "Open system log" 6451 6470 msgstr "" 6452 6471 ··· 6509 6528 msgid "Opens GIF select dialog" 6510 6529 msgstr "" 6511 6530 6512 - #: src/screens/Settings/Settings.tsx:251 6531 + #: src/screens/Settings/Settings.tsx:255 6513 6532 msgid "Opens helpdesk in browser" 6514 6533 msgstr "" 6515 6534 ··· 6966 6985 msgid "Post" 6967 6986 msgstr "" 6968 6987 6969 - #: src/view/screens/Profile.tsx:469 6970 - #: src/view/screens/Profile.tsx:470 6988 + #: src/view/screens/Profile.tsx:475 6989 + #: src/view/screens/Profile.tsx:476 6971 6990 msgid "Post a photo" 6972 6991 msgstr "" 6973 6992 6974 - #: src/view/screens/Profile.tsx:491 6975 - #: src/view/screens/Profile.tsx:492 6993 + #: src/view/screens/Profile.tsx:501 6994 + #: src/view/screens/Profile.tsx:502 6976 6995 msgid "Post a video" 6977 6996 msgstr "" 6978 6997 ··· 7116 7135 msgid "Privacy" 7117 7136 msgstr "" 7118 7137 7119 - #: src/screens/Settings/Settings.tsx:186 7120 - #: src/screens/Settings/Settings.tsx:189 7138 + #: src/screens/Settings/Settings.tsx:188 7139 + #: src/screens/Settings/Settings.tsx:191 7121 7140 msgid "Privacy and security" 7122 7141 msgstr "" 7123 7142 ··· 7395 7414 #: src/components/StarterPack/Wizard/WizardListCard.tsx:105 7396 7415 #: src/components/StarterPack/Wizard/WizardListCard.tsx:112 7397 7416 #: src/screens/Bookmarks/index.tsx:266 7398 - #: src/screens/Settings/Settings.tsx:678 7417 + #: src/screens/Settings/Settings.tsx:682 7399 7418 #: src/view/com/modals/UserAddRemoveLists.tsx:235 7400 7419 #: src/view/com/posts/PostFeedErrorMessage.tsx:220 7401 7420 msgid "Remove" ··· 7409 7428 msgid "Remove {historyItem}" 7410 7429 msgstr "" 7411 7430 7412 - #: src/screens/Settings/Settings.tsx:657 7413 - #: src/screens/Settings/Settings.tsx:660 7431 + #: src/screens/Settings/Settings.tsx:661 7432 + #: src/screens/Settings/Settings.tsx:664 7414 7433 msgid "Remove account" 7415 7434 msgstr "" 7416 7435 ··· 7455 7474 msgid "Remove from my feeds" 7456 7475 msgstr "" 7457 7476 7458 - #: src/screens/Settings/Settings.tsx:670 7477 + #: src/screens/Settings/Settings.tsx:674 7459 7478 msgid "Remove from quick access?" 7460 7479 msgstr "" 7461 7480 ··· 7499 7518 msgid "Remove subtitle file" 7500 7519 msgstr "" 7501 7520 7502 - #: src/components/contacts/screens/ViewMatches.tsx:499 7521 + #: src/components/contacts/screens/ViewMatches.tsx:497 7503 7522 #: src/screens/Settings/FindContactsSettings.tsx:323 7504 7523 msgid "Remove suggestion" 7505 7524 msgstr "" ··· 7830 7849 msgid "Resend Verification Email" 7831 7850 msgstr "" 7832 7851 7833 - #: src/screens/Settings/Settings.tsx:496 7834 - #: src/screens/Settings/Settings.tsx:498 7852 + #: src/screens/Settings/Settings.tsx:500 7853 + #: src/screens/Settings/Settings.tsx:502 7835 7854 msgid "Reset activity subscription nudge" 7836 7855 msgstr "" 7837 7856 ··· 7839 7858 msgid "Reset code" 7840 7859 msgstr "" 7841 7860 7842 - #: src/screens/Settings/Settings.tsx:481 7843 - #: src/screens/Settings/Settings.tsx:483 7861 + #: src/screens/Settings/Settings.tsx:485 7862 + #: src/screens/Settings/Settings.tsx:487 7844 7863 msgid "Reset onboarding state" 7845 7864 msgstr "" 7846 7865 ··· 7916 7935 msgid "Returns to the previous step" 7917 7936 msgstr "" 7918 7937 7919 - #: src/components/dialogs/BirthDateSettings.tsx:190 7938 + #: src/components/dialogs/BirthDateSettings.tsx:203 7920 7939 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:292 7921 7940 #: src/components/dialogs/lists/CreateOrEditListDialog.tsx:307 7922 7941 #: src/components/dialogs/PostInteractionSettingsDialog.tsx:662 ··· 7942 7961 msgid "Save" 7943 7962 msgstr "" 7944 7963 7945 - #: src/components/dialogs/BirthDateSettings.tsx:183 7964 + #: src/components/dialogs/BirthDateSettings.tsx:196 7946 7965 msgid "Save birthdate" 7947 7966 msgstr "" 7948 7967 ··· 8042 8061 msgid "Search by name or interest" 8043 8062 msgstr "" 8044 8063 8045 - #: src/components/contacts/screens/ViewMatches.tsx:355 8064 + #: src/components/contacts/screens/ViewMatches.tsx:353 8046 8065 msgid "Search contacts" 8047 8066 msgstr "" 8048 8067 ··· 8149 8168 msgid "See jobs at Bluesky" 8150 8169 msgstr "" 8151 8170 8152 - #: src/components/FeedInterstitials.tsx:499 8153 - #: src/components/FeedInterstitials.tsx:562 8171 + #: src/components/FeedInterstitials.tsx:730 8172 + #: src/components/FeedInterstitials.tsx:793 8154 8173 msgid "See more" 8155 8174 msgstr "" 8156 8175 8157 - #: src/components/FeedInterstitials.tsx:481 8176 + #: src/components/FeedInterstitials.tsx:712 8158 8177 msgid "See more suggested profiles" 8159 8178 msgstr "" 8160 8179 ··· 8281 8300 msgid "Select subtitle file (.vtt)" 8282 8301 msgstr "" 8283 8302 8284 - #: src/components/InternationalPhoneCodeSelect.tsx:67 8303 + #: src/components/InternationalPhoneCodeSelect.tsx:68 8285 8304 msgid "Select telephone code" 8286 8305 msgstr "" 8287 8306 ··· 8421 8440 msgid "Set who can reply to your post" 8422 8441 msgstr "" 8423 8442 8424 - #: src/ageAssurance/components/NoAccessScreen.tsx:199 8443 + #: src/ageAssurance/components/NoAccessScreen.tsx:211 8425 8444 msgid "Set your birthdate below and we'll get you back to posting and exploring in no time!" 8426 8445 msgstr "" 8427 8446 ··· 8430 8449 msgstr "" 8431 8450 8432 8451 #: src/Navigation.tsx:215 8433 - #: src/screens/Settings/Settings.tsx:103 8452 + #: src/screens/Settings/Settings.tsx:105 8434 8453 #: src/view/shell/desktop/LeftNav.tsx:805 8435 8454 #: src/view/shell/Drawer.tsx:609 8436 8455 msgid "Settings" ··· 8680 8699 msgid "Shows information about when this post was created" 8681 8700 msgstr "" 8682 8701 8683 - #: src/screens/Settings/Settings.tsx:129 8702 + #: src/screens/Settings/Settings.tsx:131 8684 8703 msgid "Shows other accounts you can switch to" 8685 8704 msgstr "" 8686 8705 ··· 8741 8760 msgid "Sign in to view post" 8742 8761 msgstr "" 8743 8762 8744 - #: src/screens/Settings/Settings.tsx:268 8745 - #: src/screens/Settings/Settings.tsx:270 8746 - #: src/screens/Settings/Settings.tsx:302 8763 + #: src/screens/Settings/Settings.tsx:272 8764 + #: src/screens/Settings/Settings.tsx:274 8765 + #: src/screens/Settings/Settings.tsx:306 8747 8766 #: src/screens/SignupQueued.tsx:93 8748 8767 #: src/screens/SignupQueued.tsx:96 8749 8768 #: src/screens/Takendown.tsx:93 ··· 8757 8776 msgid "Sign Out" 8758 8777 msgstr "" 8759 8778 8760 - #: src/screens/Settings/Settings.tsx:299 8779 + #: src/screens/Settings/Settings.tsx:303 8761 8780 #: src/view/shell/desktop/LeftNav.tsx:209 8762 8781 msgid "Sign out?" 8763 8782 msgstr "" ··· 8772 8791 msgid "Signed in as @{0}" 8773 8792 msgstr "" 8774 8793 8775 - #: src/components/FeedInterstitials.tsx:476 8794 + #: src/components/FeedInterstitials.tsx:707 8776 8795 msgid "Similar accounts" 8777 8796 msgstr "" 8778 8797 ··· 8823 8842 msgid "Some of your verifications are invalid." 8824 8843 msgstr "" 8825 8844 8826 - #: src/components/FeedInterstitials.tsx:639 8845 + #: src/components/FeedInterstitials.tsx:870 8827 8846 msgid "Some other feeds you might like" 8828 8847 msgstr "" 8829 8848 ··· 8880 8899 msgid "Sorry, we're unable to load account suggestions at this time." 8881 8900 msgstr "" 8882 8901 8883 - #: src/App.native.tsx:127 8884 - #: src/App.web.tsx:100 8902 + #: src/App.native.tsx:126 8903 + #: src/App.web.tsx:99 8885 8904 msgid "Sorry! Your session expired. Please sign in again." 8886 8905 msgstr "" 8887 8906 ··· 8969 8988 msgid "Starter packs let you easily share your favorite feeds and people with your friends." 8970 8989 msgstr "" 8971 8990 8972 - #: src/view/screens/Profile.tsx:539 8973 - msgid "Starter packs let you share your favorite feeds and people with your friends." 8991 + #: src/view/screens/Profile.tsx:553 8992 + msgid "Starter Packs let you share your favorite feeds and people with your friends." 8974 8993 msgstr "" 8975 8994 8976 8995 #: src/screens/Settings/AboutSettings.tsx:100 ··· 8983 9002 msgid "Step {0} of {1}" 8984 9003 msgstr "" 8985 9004 8986 - #: src/screens/Settings/Settings.tsx:407 9005 + #: src/screens/Settings/Settings.tsx:411 8987 9006 msgid "Storage cleared, you need to restart the app now." 8988 9007 msgstr "" 8989 9008 ··· 8992 9011 msgstr "" 8993 9012 8994 9013 #: src/Navigation.tsx:308 8995 - #: src/screens/Settings/Settings.tsx:462 9014 + #: src/screens/Settings/Settings.tsx:466 8996 9015 msgid "Storybook" 8997 9016 msgstr "" 8998 9017 ··· 9062 9081 msgstr "" 9063 9082 9064 9083 #. Accounts suggested to the user for them to follow 9065 - #: src/components/FeedInterstitials.tsx:474 9084 + #: src/components/FeedInterstitials.tsx:705 9066 9085 #: src/screens/Onboarding/StepSuggestedAccounts/index.tsx:155 9067 9086 msgid "Suggested for you" 9068 9087 msgstr "" ··· 9092 9111 msgid "Support for this feature in your country has not been enabled yet! Please check back later." 9093 9112 msgstr "" 9094 9113 9095 - #: src/screens/Settings/Settings.tsx:127 9096 - #: src/screens/Settings/Settings.tsx:141 9097 - #: src/screens/Settings/Settings.tsx:620 9114 + #: src/screens/Settings/Settings.tsx:129 9115 + #: src/screens/Settings/Settings.tsx:143 9116 + #: src/screens/Settings/Settings.tsx:624 9098 9117 #: src/view/shell/desktop/LeftNav.tsx:247 9099 9118 msgid "Switch account" 9100 9119 msgstr "" ··· 9122 9141 #: src/screens/Log.tsx:58 9123 9142 #: src/screens/Settings/AboutSettings.tsx:107 9124 9143 #: src/screens/Settings/AboutSettings.tsx:110 9125 - #: src/screens/Settings/Settings.tsx:455 9144 + #: src/screens/Settings/Settings.tsx:459 9126 9145 msgid "System log" 9127 9146 msgstr "" 9128 9147 ··· 9193 9212 msgid "Terms" 9194 9213 msgstr "" 9195 9214 9196 - #: src/components/dialogs/BirthDateSettings.tsx:169 9215 + #: src/components/dialogs/BirthDateSettings.tsx:182 9197 9216 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:30 9198 9217 #: src/components/PolicyUpdateOverlay/updates/202508/index.tsx:97 9199 9218 #: src/Navigation.tsx:338 ··· 9222 9241 msgid "Thanks, you have successfully verified your email address. You can close this dialog." 9223 9242 msgstr "" 9224 9243 9225 - #: src/ageAssurance/components/NoAccessScreen.tsx:382 9244 + #: src/ageAssurance/components/NoAccessScreen.tsx:385 9226 9245 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:113 9227 9246 msgid "Thanks! You're all set." 9228 9247 msgstr "" ··· 9268 9287 msgid "The author of this thread has hidden this reply." 9269 9288 msgstr "" 9270 9289 9271 - #: src/components/dialogs/BirthDateSettings.tsx:156 9290 + #: src/components/dialogs/BirthDateSettings.tsx:169 9272 9291 msgid "The birthdate you've entered means you are under 18 years old. Certain content and features may be unavailable to you." 9273 9292 msgstr "" 9274 9293 ··· 9364 9383 msgid "Theme" 9365 9384 msgstr "" 9366 9385 9367 - #: src/components/dialogs/BirthDateSettings.tsx:91 9386 + #: src/components/dialogs/BirthDateSettings.tsx:104 9368 9387 msgid "There is a limit to how often you can change your birthdate. You may need to wait a day or two before updating it again." 9369 9388 msgstr "" 9370 9389 ··· 9409 9428 msgid "There was an issue fetching your app passwords" 9410 9429 msgstr "" 9411 9430 9412 - #: src/view/com/feeds/ProfileFeedgens.tsx:174 9413 - #: src/view/com/lists/ProfileLists.tsx:175 9431 + #: src/view/com/feeds/ProfileFeedgens.tsx:185 9432 + #: src/view/com/lists/ProfileLists.tsx:184 9414 9433 msgid "There was an issue fetching your lists. Tap here to try again." 9415 9434 msgstr "" 9416 9435 ··· 9588 9607 msgid "This handle is reserved. Please try a different one." 9589 9608 msgstr "" 9590 9609 9591 - #: src/components/dialogs/BirthDateSettings.tsx:51 9610 + #: src/components/dialogs/BirthDateSettings.tsx:55 9592 9611 msgid "This information is private and not shared with other users." 9593 9612 msgstr "" 9594 9613 ··· 9718 9737 msgid "This will delete \"{0}\" from your muted words. You can always add it back later." 9719 9738 msgstr "" 9720 9739 9721 - #: src/screens/Settings/Settings.tsx:672 9740 + #: src/screens/Settings/Settings.tsx:676 9722 9741 msgid "This will remove @{0} from the quick access list." 9723 9742 msgstr "" 9724 9743 ··· 9766 9785 msgid "To disable your email 2FA method, please verify your access to <0>{0}</0>" 9767 9786 msgstr "" 9768 9787 9769 - #: src/ageAssurance/components/NoAccessScreen.tsx:230 9788 + #: src/ageAssurance/components/NoAccessScreen.tsx:233 9770 9789 msgid "To log out, <0>click here</0>." 9771 9790 msgstr "" 9772 9791 ··· 9897 9916 msgid "Unable to delete" 9898 9917 msgstr "" 9899 9918 9900 - #: src/screens/Settings/Settings.tsx:521 9919 + #: src/screens/Settings/Settings.tsx:525 9901 9920 msgid "Unapply Pull Request" 9902 9921 msgstr "" 9903 9922 9904 - #: src/screens/Settings/Settings.tsx:523 9923 + #: src/screens/Settings/Settings.tsx:527 9905 9924 msgid "Unapply Pull Request {currentChannel}" 9906 9925 msgstr "" 9907 9926 ··· 9981 10000 msgid "Unfortunately, none of your subscribed labelers supports this report type." 9982 10001 msgstr "" 9983 10002 9984 - #: src/ageAssurance/components/NoAccessScreen.tsx:176 10003 + #: src/ageAssurance/components/NoAccessScreen.tsx:188 9985 10004 msgid "Unfortunately, the birthdate you have saved to your profile makes you too young to access Bluesky." 9986 10005 msgstr "" 9987 10006 ··· 10089 10108 msgid "Unpinned list" 10090 10109 msgstr "" 10091 10110 10092 - #: src/screens/Settings/Settings.tsx:488 10093 - #: src/screens/Settings/Settings.tsx:490 10111 + #: src/screens/Settings/Settings.tsx:492 10112 + #: src/screens/Settings/Settings.tsx:494 10094 10113 msgid "Unsnooze email reminder" 10095 10114 msgstr "" 10096 10115 ··· 10342 10361 msgid "Verify account" 10343 10362 msgstr "" 10344 10363 10345 - #: src/ageAssurance/components/NoAccessScreen.tsx:319 10364 + #: src/ageAssurance/components/NoAccessScreen.tsx:322 10346 10365 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:168 10347 10366 msgid "Verify again" 10348 10367 msgstr "" ··· 10369 10388 msgid "Verify email dialog" 10370 10389 msgstr "" 10371 10390 10372 - #: src/ageAssurance/components/NoAccessScreen.tsx:307 10373 - #: src/ageAssurance/components/NoAccessScreen.tsx:321 10391 + #: src/ageAssurance/components/NoAccessScreen.tsx:310 10392 + #: src/ageAssurance/components/NoAccessScreen.tsx:324 10374 10393 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:156 10375 10394 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:170 10376 10395 msgid "Verify now" ··· 10709 10728 msgid "We were unable to determine if you are allowed to upload videos. Please try again." 10710 10729 msgstr "" 10711 10730 10712 - #: src/components/dialogs/BirthDateSettings.tsx:63 10731 + #: src/components/dialogs/BirthDateSettings.tsx:67 10713 10732 msgid "We were unable to load your birthdate preferences. Please try again." 10714 10733 msgstr "" 10715 10734 ··· 10768 10787 msgid "We're so excited to have you join us!" 10769 10788 msgstr "" 10770 10789 10771 - #: src/ageAssurance/components/NoAccessScreen.tsx:375 10790 + #: src/ageAssurance/components/NoAccessScreen.tsx:378 10772 10791 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:106 10773 10792 msgid "We're sorry, but based on your device's location, you are currently located in a region that requires age assurance." 10774 10793 msgstr "" ··· 10916 10935 msgid "Write a message" 10917 10936 msgstr "" 10918 10937 10919 - #: src/view/screens/Profile.tsx:433 10920 - #: src/view/screens/Profile.tsx:434 10938 + #: src/view/screens/Profile.tsx:435 10939 + #: src/view/screens/Profile.tsx:436 10921 10940 msgid "Write a post" 10922 10941 msgstr "" 10923 10942 ··· 10976 10995 msgid "You are a trusted verifier" 10977 10996 msgstr "" 10978 10997 10979 - #: src/ageAssurance/components/NoAccessScreen.tsx:161 10998 + #: src/ageAssurance/components/NoAccessScreen.tsx:173 10980 10999 msgid "You are accessing Bluesky from a region that legally requires us to verify your age before allowing you to access the app." 10981 11000 msgstr "" 10982 11001 ··· 10984 11003 msgid "You are creating an account on" 10985 11004 msgstr "" 10986 11005 10987 - #: src/ageAssurance/components/NoAccessScreen.tsx:289 11006 + #: src/ageAssurance/components/NoAccessScreen.tsx:292 10988 11007 #: src/components/ageAssurance/AgeAssuranceAccountCard.tsx:126 10989 11008 msgid "You are currently unable to access Bluesky's Age Assurance flow. Please <0>contact our moderation team</0> if you believe this is an error." 10990 11009 msgstr "" ··· 11117 11136 msgid "You don't have any saved feeds." 11118 11137 msgstr "" 11119 11138 11120 - #: src/components/contacts/screens/ViewMatches.tsx:311 11139 + #: src/components/contacts/screens/ViewMatches.tsx:309 11121 11140 msgid "You got here first" 11122 11141 msgstr "" 11123 11142 ··· 11194 11213 msgid "You haven't created a starter pack yet!" 11195 11214 msgstr "" 11196 11215 11197 - #: src/view/com/feeds/ProfileFeedgens.tsx:155 11216 + #: src/view/com/lists/ProfileLists.tsx:159 11217 + msgid "You haven't created any lists yet." 11218 + msgstr "" 11219 + 11220 + #: src/view/com/feeds/ProfileFeedgens.tsx:160 11198 11221 msgid "You haven't made any custom feeds yet." 11199 11222 msgstr "" 11200 11223 ··· 11239 11262 msgid "You must be 13 years of age or older to create an account." 11240 11263 msgstr "" 11241 11264 11242 - #: src/components/dialogs/BirthDateSettings.tsx:165 11265 + #: src/components/dialogs/BirthDateSettings.tsx:178 11243 11266 msgid "You must be at least 13 years old to use Bluesky. Read our <0>Terms of Service</0> for more information." 11244 11267 msgstr "" 11245 11268 ··· 11272 11295 msgid "You previously deactivated @{0}." 11273 11296 msgstr "" 11274 11297 11275 - #: src/screens/Settings/Settings.tsx:418 11298 + #: src/screens/Settings/Settings.tsx:422 11276 11299 msgid "You probably want to restart the app now." 11277 11300 msgstr "" 11278 11301 ··· 11284 11307 msgid "You reacted {0} to {1}" 11285 11308 msgstr "" 11286 11309 11287 - #: src/components/dialogs/BirthDateSettings.tsx:77 11288 - #: src/components/dialogs/BirthDateSettings.tsx:87 11310 + #: src/components/dialogs/BirthDateSettings.tsx:90 11311 + #: src/components/dialogs/BirthDateSettings.tsx:100 11289 11312 msgid "You recently changed your birthdate" 11290 11313 msgstr "" 11291 11314 11292 - #: src/screens/Settings/Settings.tsx:300 11315 + #: src/screens/Settings/Settings.tsx:304 11293 11316 #: src/view/shell/desktop/LeftNav.tsx:210 11294 11317 msgid "You will be signed out of all your accounts." 11295 11318 msgstr "" ··· 11456 11479 msgid "Your contact {firstAuthorName} is on Bluesky" 11457 11480 msgstr "" 11458 11481 11459 - #: src/components/contacts/screens/ViewMatches.tsx:453 11482 + #: src/components/contacts/screens/ViewMatches.tsx:451 11460 11483 msgid "Your contact {name}" 11461 11484 msgstr "" 11462 11485
+6
src/logger/metrics.ts
··· 379 379 | 'Profile' 380 380 | 'Onboarding' 381 381 } 382 + 'suggestedUser:dismiss': { 383 + logContext: 'InterstitialDiscover' | 'InterstitialProfile' 384 + recId?: number 385 + position: number 386 + suggestedDid: string 387 + } 382 388 'profile:unfollow': { 383 389 logContext: 384 390 | 'RecommendedFollowsItem'
+1 -1
src/routes.ts
··· 7 7 > 8 8 9 9 export const router = new Router<AllNavigatableRoutes>({ 10 - Home: '/', 10 + Home: ['/', '/download'], 11 11 Search: '/search', 12 12 Feeds: '/feeds', 13 13 Notifications: '/notifications',
+181 -5
src/screens/Profile/Header/SuggestedFollows.tsx
··· 1 + import React from 'react' 2 + import {type AppBskyActorDefs} from '@atproto/api' 3 + 1 4 import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' 2 5 import {isAndroid} from '#/platform/detection' 3 - import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' 6 + import {useModerationOpts} from '#/state/preferences/moderation-opts' 7 + import { 8 + useSuggestedFollowsByActorQuery, 9 + useSuggestedFollowsQuery, 10 + } from '#/state/queries/suggested-follows' 11 + import {useBreakpoints} from '#/alf' 4 12 import {ProfileGrid} from '#/components/FeedInterstitials' 13 + 14 + const DISMISS_ANIMATION_DURATION = 200 5 15 6 16 export function ProfileHeaderSuggestedFollows({actorDid}: {actorDid: string}) { 17 + const {gtMobile} = useBreakpoints() 18 + const moderationOpts = useModerationOpts() 19 + const maxLength = gtMobile ? 4 : 12 7 20 const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ 8 21 did: actorDid, 9 22 }) 23 + const { 24 + data: moreSuggestions, 25 + fetchNextPage, 26 + hasNextPage, 27 + isFetchingNextPage, 28 + } = useSuggestedFollowsQuery({limit: 25}) 29 + 30 + const [dismissedDids, setDismissedDids] = React.useState<Set<string>>( 31 + new Set(), 32 + ) 33 + const [dismissingDids, setDismissingDids] = React.useState<Set<string>>( 34 + new Set(), 35 + ) 36 + 37 + const onDismiss = React.useCallback((did: string) => { 38 + // Start the fade animation 39 + setDismissingDids(prev => new Set(prev).add(did)) 40 + // After animation completes, actually remove from list 41 + setTimeout(() => { 42 + setDismissedDids(prev => new Set(prev).add(did)) 43 + setDismissingDids(prev => { 44 + const next = new Set(prev) 45 + next.delete(did) 46 + return next 47 + }) 48 + }, DISMISS_ANIMATION_DURATION) 49 + }, []) 50 + 51 + // Combine profiles from the actor-specific query with fallback suggestions 52 + const allProfiles = React.useMemo(() => { 53 + const actorProfiles = data?.suggestions ?? [] 54 + const fallbackProfiles = 55 + moreSuggestions?.pages.flatMap(page => page.actors) ?? [] 56 + 57 + // Dedupe by did, preferring actor-specific profiles 58 + const seen = new Set<string>() 59 + const combined: AppBskyActorDefs.ProfileView[] = [] 60 + 61 + for (const profile of actorProfiles) { 62 + if (!seen.has(profile.did)) { 63 + seen.add(profile.did) 64 + combined.push(profile) 65 + } 66 + } 67 + 68 + for (const profile of fallbackProfiles) { 69 + if (!seen.has(profile.did) && profile.did !== actorDid) { 70 + seen.add(profile.did) 71 + combined.push(profile) 72 + } 73 + } 74 + 75 + return combined 76 + }, [data?.suggestions, moreSuggestions?.pages, actorDid]) 77 + 78 + const filteredProfiles = React.useMemo(() => { 79 + return allProfiles.filter(p => !dismissedDids.has(p.did)) 80 + }, [allProfiles, dismissedDids]) 81 + 82 + // Fetch more when running low 83 + React.useEffect(() => { 84 + if ( 85 + moderationOpts && 86 + filteredProfiles.length < maxLength && 87 + hasNextPage && 88 + !isFetchingNextPage 89 + ) { 90 + fetchNextPage() 91 + } 92 + }, [ 93 + filteredProfiles.length, 94 + maxLength, 95 + hasNextPage, 96 + isFetchingNextPage, 97 + fetchNextPage, 98 + moderationOpts, 99 + ]) 10 100 11 101 return ( 12 102 <ProfileGrid 13 103 isSuggestionsLoading={isLoading} 14 - profiles={data?.suggestions ?? []} 104 + profiles={filteredProfiles} 105 + totalProfileCount={allProfiles.length} 15 106 recId={data?.recId} 16 107 error={error} 17 108 viewContext="profileHeader" 109 + onDismiss={onDismiss} 110 + dismissingDids={dismissingDids} 18 111 /> 19 112 ) 20 113 } ··· 26 119 isExpanded: boolean 27 120 actorDid: string 28 121 }) { 122 + const {gtMobile} = useBreakpoints() 123 + const moderationOpts = useModerationOpts() 124 + const maxLength = gtMobile ? 4 : 12 29 125 const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ 30 126 did: actorDid, 31 127 }) 128 + const { 129 + data: moreSuggestions, 130 + fetchNextPage, 131 + hasNextPage, 132 + isFetchingNextPage, 133 + } = useSuggestedFollowsQuery({limit: 25}) 32 134 33 - if (!data?.suggestions?.length) return null 135 + const [dismissedDids, setDismissedDids] = React.useState<Set<string>>( 136 + new Set(), 137 + ) 138 + const [dismissingDids, setDismissingDids] = React.useState<Set<string>>( 139 + new Set(), 140 + ) 141 + 142 + const onDismiss = React.useCallback((did: string) => { 143 + // Start the fade animation 144 + setDismissingDids(prev => new Set(prev).add(did)) 145 + // After animation completes, actually remove from list 146 + setTimeout(() => { 147 + setDismissedDids(prev => new Set(prev).add(did)) 148 + setDismissingDids(prev => { 149 + const next = new Set(prev) 150 + next.delete(did) 151 + return next 152 + }) 153 + }, DISMISS_ANIMATION_DURATION) 154 + }, []) 155 + 156 + // Combine profiles from the actor-specific query with fallback suggestions 157 + const allProfiles = React.useMemo(() => { 158 + const actorProfiles = data?.suggestions ?? [] 159 + const fallbackProfiles = 160 + moreSuggestions?.pages.flatMap(page => page.actors) ?? [] 161 + 162 + // Dedupe by did, preferring actor-specific profiles 163 + const seen = new Set<string>() 164 + const combined: AppBskyActorDefs.ProfileView[] = [] 165 + 166 + for (const profile of actorProfiles) { 167 + if (!seen.has(profile.did)) { 168 + seen.add(profile.did) 169 + combined.push(profile) 170 + } 171 + } 172 + 173 + for (const profile of fallbackProfiles) { 174 + if (!seen.has(profile.did) && profile.did !== actorDid) { 175 + seen.add(profile.did) 176 + combined.push(profile) 177 + } 178 + } 179 + 180 + return combined 181 + }, [data?.suggestions, moreSuggestions?.pages, actorDid]) 182 + 183 + const filteredProfiles = React.useMemo(() => { 184 + return allProfiles.filter(p => !dismissedDids.has(p.did)) 185 + }, [allProfiles, dismissedDids]) 186 + 187 + // Fetch more when running low 188 + React.useEffect(() => { 189 + if ( 190 + moderationOpts && 191 + filteredProfiles.length < maxLength && 192 + hasNextPage && 193 + !isFetchingNextPage 194 + ) { 195 + fetchNextPage() 196 + } 197 + }, [ 198 + filteredProfiles.length, 199 + maxLength, 200 + hasNextPage, 201 + isFetchingNextPage, 202 + fetchNextPage, 203 + moderationOpts, 204 + ]) 205 + 206 + if (!allProfiles.length && !isLoading) return null 34 207 35 208 /* NOTE (caidanw): 36 209 * Android does not work well with this feature yet. ··· 43 216 <AccordionAnimation isExpanded={isExpanded}> 44 217 <ProfileGrid 45 218 isSuggestionsLoading={isLoading} 46 - profiles={data.suggestions} 47 - recId={data.recId} 219 + profiles={filteredProfiles} 220 + totalProfileCount={allProfiles.length} 221 + recId={data?.recId} 48 222 error={error} 49 223 viewContext="profileHeader" 224 + onDismiss={onDismiss} 225 + dismissingDids={dismissingDids} 50 226 isVisible={isExpanded} 51 227 /> 52 228 </AccordionAnimation>
+14 -10
src/screens/Settings/Settings.tsx
··· 16 16 type CommonNavigatorParams, 17 17 type NavigationProp, 18 18 } from '#/lib/routes/types' 19 + import {useGate} from '#/lib/statsig/statsig' 19 20 import {sanitizeDisplayName} from '#/lib/strings/display-names' 20 21 import {sanitizeHandle} from '#/lib/strings/handles' 21 22 import {isIOS, isNative} from '#/platform/detection' ··· 95 96 const [showDevOptions, setShowDevOptions] = useState(false) 96 97 const findContactsEnabled = 97 98 useIsFindContactsFeatureEnabledBasedOnGeolocation() 99 + const gate = useGate() 98 100 99 101 return ( 100 102 <Layout.Screen> ··· 213 215 <Trans>Content and media</Trans> 214 216 </SettingsList.ItemText> 215 217 </SettingsList.LinkItem> 216 - {isNative && findContactsEnabled && ( 217 - <SettingsList.LinkItem 218 - to="/settings/find-contacts" 219 - label={_(msg`Find friends from contacts`)}> 220 - <SettingsList.ItemIcon icon={ContactsIcon} /> 221 - <SettingsList.ItemText> 222 - <Trans>Find friends from contacts</Trans> 223 - </SettingsList.ItemText> 224 - </SettingsList.LinkItem> 225 - )} 218 + {isNative && 219 + findContactsEnabled && 220 + !gate('disable_settings_find_contacts') && ( 221 + <SettingsList.LinkItem 222 + to="/settings/find-contacts" 223 + label={_(msg`Find friends from contacts`)}> 224 + <SettingsList.ItemIcon icon={ContactsIcon} /> 225 + <SettingsList.ItemText> 226 + <Trans>Find friends from contacts</Trans> 227 + </SettingsList.ItemText> 228 + </SettingsList.LinkItem> 229 + )} 226 230 <SettingsList.LinkItem 227 231 to="/settings/appearance" 228 232 label={_(msg`Appearance`)}>
+29 -9
src/view/com/feeds/ProfileFeedgens.tsx
··· 23 23 import {isIOS, isNative, isWeb} from '#/platform/detection' 24 24 import {usePreferencesQuery} from '#/state/queries/preferences' 25 25 import {RQKEY, useProfileFeedgensQuery} from '#/state/queries/profile-feedgens' 26 + import {useSession} from '#/state/session' 26 27 import {EmptyState} from '#/view/com/util/EmptyState' 27 28 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 28 29 import {List, type ListRef} from '#/view/com/util/List' ··· 81 82 const isEmpty = !isPending && !data?.pages[0]?.feeds.length 82 83 const {data: preferences} = usePreferencesQuery() 83 84 const navigation = useNavigation() 85 + const {currentAccount} = useSession() 86 + const isSelf = currentAccount?.did === did 84 87 85 88 const items = useMemo(() => { 86 89 let items: any[] = [] ··· 152 155 <EmptyState 153 156 style={{width: '100%'}} 154 157 icon={HashtagWideIcon} 155 - message={_(msg`You haven't made any custom feeds yet.`)} 158 + message={ 159 + isSelf 160 + ? _(msg`You haven't made any custom feeds yet.`) 161 + : _(msg`No custom feeds yet`) 162 + } 156 163 textStyle={[t.atoms.text_contrast_medium, a.font_medium]} 157 - button={{ 158 - label: _(msg`Browse custom feeds`), 159 - text: _(msg`Browse custom feeds`), 160 - onPress: () => navigation.navigate('Feeds' as never), 161 - size: 'small', 162 - color: 'secondary', 163 - }} 164 + button={ 165 + isSelf 166 + ? { 167 + label: _(msg`Browse custom feeds`), 168 + text: _(msg`Browse custom feeds`), 169 + onPress: () => navigation.navigate('Feeds' as never), 170 + size: 'small', 171 + color: 'secondary', 172 + } 173 + : undefined 174 + } 164 175 /> 165 176 ) 166 177 } else if (item === ERROR_ITEM) { ··· 194 205 } 195 206 return null 196 207 }, 197 - [_, t, error, refetch, onPressRetryLoadMore, preferences, navigation], 208 + [ 209 + _, 210 + t, 211 + error, 212 + refetch, 213 + onPressRetryLoadMore, 214 + preferences, 215 + navigation, 216 + isSelf, 217 + ], 198 218 ) 199 219 200 220 useEffect(() => {
+29 -11
src/view/com/lists/ProfileLists.tsx
··· 23 23 import {isIOS, isNative, isWeb} from '#/platform/detection' 24 24 import {usePreferencesQuery} from '#/state/queries/preferences' 25 25 import {RQKEY, useProfileListsQuery} from '#/state/queries/profile-lists' 26 + import {useSession} from '#/state/session' 26 27 import {EmptyState} from '#/view/com/util/EmptyState' 27 28 import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 28 29 import {List, type ListRef} from '#/view/com/util/List' ··· 81 82 const isEmpty = !isPending && !data?.pages[0]?.lists.length 82 83 const {data: preferences} = usePreferencesQuery() 83 84 const navigation = useNavigation() 85 + const {currentAccount} = useSession() 86 + const isSelf = currentAccount?.did === did 84 87 85 88 const items = useMemo(() => { 86 89 let listItems: any[] = [] ··· 151 154 return ( 152 155 <EmptyState 153 156 icon={ListIcon} 154 - message={_( 155 - msg`Lists allow you to see content from your favorite people.`, 156 - )} 157 + message={ 158 + isSelf 159 + ? _(msg`You haven't created any lists yet.`) 160 + : _(msg`No lists`) 161 + } 157 162 textStyle={[t.atoms.text_contrast_medium, a.font_medium]} 158 - button={{ 159 - label: _(msg`Create a list`), 160 - text: _(msg`Create a list`), 161 - onPress: () => navigation.navigate('Lists' as never), 162 - size: 'small', 163 - color: 'primary', 164 - }} 163 + button={ 164 + isSelf 165 + ? { 166 + label: _(msg`Create a list`), 167 + text: _(msg`Create a list`), 168 + onPress: () => navigation.navigate('Lists' as never), 169 + size: 'small', 170 + color: 'primary', 171 + } 172 + : undefined 173 + } 165 174 /> 166 175 ) 167 176 } else if (item === ERROR_ITEM) { ··· 195 204 } 196 205 return null 197 206 }, 198 - [_, t, error, refetch, onPressRetryLoadMore, preferences, navigation], 207 + [ 208 + _, 209 + t, 210 + error, 211 + refetch, 212 + onPressRetryLoadMore, 213 + preferences, 214 + navigation, 215 + isSelf, 216 + ], 199 217 ) 200 218 201 219 useEffect(() => {
+51 -31
src/view/screens/Profile.tsx
··· 429 429 ignoreFilterFor={profile.did} 430 430 setScrollViewTag={setScrollViewTag} 431 431 emptyStateMessage={_(msg`No posts yet`)} 432 - emptyStateButton={{ 433 - label: _(msg`Write a post`), 434 - text: _(msg`Write a post`), 435 - onPress: () => openComposer({}), 436 - size: 'small', 437 - color: 'primary', 438 - }} 432 + emptyStateButton={ 433 + isMe 434 + ? { 435 + label: _(msg`Write a post`), 436 + text: _(msg`Write a post`), 437 + onPress: () => openComposer({}), 438 + size: 'small', 439 + color: 'primary', 440 + } 441 + : undefined 442 + } 439 443 /> 440 444 ) 441 445 : null} ··· 465 469 ignoreFilterFor={profile.did} 466 470 setScrollViewTag={setScrollViewTag} 467 471 emptyStateMessage={_(msg`No media yet`)} 468 - emptyStateButton={{ 469 - label: _(msg`Post a photo`), 470 - text: _(msg`Post a photo`), 471 - onPress: () => openComposer({}), 472 - size: 'small', 473 - color: 'primary', 474 - }} 472 + emptyStateButton={ 473 + isMe 474 + ? { 475 + label: _(msg`Post a photo`), 476 + text: _(msg`Post a photo`), 477 + onPress: () => openComposer({}), 478 + size: 'small', 479 + color: 'primary', 480 + } 481 + : undefined 482 + } 475 483 emptyStateIcon={ImageIcon} 476 484 /> 477 485 ) ··· 487 495 ignoreFilterFor={profile.did} 488 496 setScrollViewTag={setScrollViewTag} 489 497 emptyStateMessage={_(msg`No video posts yet`)} 490 - emptyStateButton={{ 491 - label: _(msg`Post a video`), 492 - text: _(msg`Post a video`), 493 - onPress: () => openComposer({}), 494 - size: 'small', 495 - color: 'primary', 496 - }} 498 + emptyStateButton={ 499 + isMe 500 + ? { 501 + label: _(msg`Post a video`), 502 + text: _(msg`Post a video`), 503 + onPress: () => openComposer({}), 504 + size: 'small', 505 + color: 'primary', 506 + } 507 + : undefined 508 + } 497 509 emptyStateIcon={VideoIcon} 498 510 /> 499 511 ) ··· 535 547 headerOffset={headerHeight} 536 548 enabled={isFocused} 537 549 setScrollViewTag={setScrollViewTag} 538 - emptyStateMessage={_( 539 - msg`Starter packs let you share your favorite feeds and people with your friends.`, 540 - )} 541 - emptyStateButton={{ 542 - label: _(msg`Create a Starter Pack`), 543 - text: _(msg`Create a Starter Pack`), 544 - onPress: wrappedNavToWizard, 545 - color: 'primary', 546 - size: 'small', 547 - }} 550 + emptyStateMessage={ 551 + isMe 552 + ? _( 553 + msg`Starter Packs let you share your favorite feeds and people with your friends.`, 554 + ) 555 + : _(msg`No Starter Packs yet`) 556 + } 557 + emptyStateButton={ 558 + isMe 559 + ? { 560 + label: _(msg`Create a Starter Pack`), 561 + text: _(msg`Create a Starter Pack`), 562 + onPress: wrappedNavToWizard, 563 + color: 'primary', 564 + size: 'small', 565 + } 566 + : undefined 567 + } 548 568 emptyStateIcon={CircleAndSquareIcon} 549 569 /> 550 570 )
+44
src/view/screens/Storybook/Forms.tsx
··· 1 1 import React from 'react' 2 2 import {type TextInput, View} from 'react-native' 3 3 4 + import {APP_LANGUAGES} from '#/lib/../locale/languages' 4 5 import {atoms as a} from '#/alf' 5 6 import {Button, ButtonText} from '#/components/Button' 6 7 import {DateField, LabelText} from '#/components/forms/DateField' ··· 9 10 import * as Toggle from '#/components/forms/Toggle' 10 11 import * as ToggleButton from '#/components/forms/ToggleButton' 11 12 import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 13 + import {InternationalPhoneCodeSelect} from '#/components/InternationalPhoneCodeSelect' 14 + import * as Select from '#/components/Select' 12 15 import {H1, H3} from '#/components/Typography' 13 16 14 17 export function Forms() { ··· 22 25 23 26 const [value, setValue] = React.useState('') 24 27 const [date, setDate] = React.useState('2001-01-01') 28 + const [countryCode, setCountryCode] = React.useState('US') 29 + const [phoneNumber, setPhoneNumber] = React.useState('') 30 + const [lang, setLang] = React.useState('en') 25 31 26 32 const inputRef = React.useRef<TextInput>(null) 27 33 28 34 return ( 29 35 <View style={[a.gap_4xl, a.align_start]}> 30 36 <H1>Forms</H1> 37 + 38 + <Select.Root value={lang} onValueChange={setLang}> 39 + <Select.Trigger label="Select app language"> 40 + <Select.ValueText /> 41 + <Select.Icon /> 42 + </Select.Trigger> 43 + <Select.Content 44 + label="App language" 45 + renderItem={({label, value}) => ( 46 + <Select.Item value={value} label={label}> 47 + <Select.ItemIndicator /> 48 + <Select.ItemText>{label}</Select.ItemText> 49 + </Select.Item> 50 + )} 51 + items={APP_LANGUAGES.map(l => ({ 52 + label: l.name, 53 + value: l.code2, 54 + }))} 55 + /> 56 + </Select.Root> 57 + 58 + <View style={[a.flex_row, a.gap_sm, a.align_center]}> 59 + <View> 60 + <InternationalPhoneCodeSelect 61 + // @ts-ignore 62 + value={countryCode} 63 + onChange={value => setCountryCode(value)} 64 + /> 65 + </View> 66 + 67 + <View style={[a.flex_1]}> 68 + <TextField.Input 69 + label="Phone number" 70 + value={phoneNumber} 71 + onChangeText={setPhoneNumber} 72 + /> 73 + </View> 74 + </View> 31 75 32 76 <View style={[a.gap_md, a.align_start, a.w_full]}> 33 77 <H3>InputText</H3>
+1 -1
src/view/shell/bottom-bar/BottomBar.tsx
··· 425 425 enableSquareButtons ? a.rounded_sm : a.rounded_full, 426 426 {backgroundColor: t.palette.primary_500}, 427 427 ]}> 428 - <Text style={styles.notificationCountLabel}>1</Text> 428 + <Text style={styles.notificationCountLabel}>{notificationCount}</Text> 429 429 </View> 430 430 ) : hasNew ? ( 431 431 <View
+2
src/view/shell/index.tsx
··· 32 32 import {InAppBrowserConsentDialog} from '#/components/dialogs/InAppBrowserConsent' 33 33 import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' 34 34 import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 35 + import {NuxDialogs} from '#/components/dialogs/nuxs' 35 36 import {SigninDialog} from '#/components/dialogs/Signin' 36 37 import { 37 38 Outlet as PolicyUpdateOverlayPortalOutlet, ··· 110 111 <InAppBrowserConsentDialog /> 111 112 <LinkWarningDialog /> 112 113 <Lightbox /> 114 + <NuxDialogs /> 113 115 114 116 {/* Until policy update has been completed by the user, don't render anything that is portaled */} 115 117 {policyUpdateState.completed && (
+2
src/view/shell/index.web.tsx
··· 22 22 import {EmailDialog} from '#/components/dialogs/EmailDialog' 23 23 import {LinkWarningDialog} from '#/components/dialogs/LinkWarning' 24 24 import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 25 + import {NuxDialogs} from '#/components/dialogs/nuxs' 25 26 import {SigninDialog} from '#/components/dialogs/Signin' 26 27 import {useWelcomeModal} from '#/components/hooks/useWelcomeModal' 27 28 import { ··· 119 120 <AgeAssuranceRedirectDialog /> 120 121 <LinkWarningDialog /> 121 122 <Lightbox /> 123 + <NuxDialogs /> 122 124 123 125 {welcomeModalControl.isOpen && ( 124 126 <WelcomeModal control={welcomeModalControl} />