+1
.gitignore
+1
.gitignore
+1
-1
package.json
+1
-1
package.json
-2
src/App.native.tsx
-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
-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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+1
-1
src/routes.ts
+181
-5
src/screens/Profile/Header/SuggestedFollows.tsx
+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
+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
+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
+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
+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
+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
+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
+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
+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} />