Add profile QR code for sharing and inviting friends (#10659)

This commit is contained in:
Spence Pope
2026-06-10 14:14:45 -04:00
committed by GitHub
parent adb39b4442
commit c6f9803676
32 changed files with 1853 additions and 55 deletions
+80 -48
View File
@@ -5,14 +5,21 @@ import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {type NavigationProp} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSession} from '#/state/session'
import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist'
import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import {
FollowersPromoBanner,
useFollowersPromoDismissed,
} from '#/features/inviteFriends'
import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from './ProfileCard'
@@ -43,7 +50,7 @@ function keyExtractor(item: ActorDefs.ProfileViewBasic) {
export function ProfileFollowers({name}: {name: string}) {
const {_} = useLingui()
const ax = useAnalytics()
const navigation = useNavigation()
const navigation = useNavigation<NavigationProp>()
const initialNumToRender = useInitialNumToRender()
const {currentAccount} = useSession()
@@ -158,55 +165,80 @@ export function ProfileFollowers({name}: {name: string}) {
[ax, followers, resolvedDid],
)
if (followers.length < 1) {
return (
<ListMaybePlaceholder
isLoading={isDidLoading || isFollowersLoading}
isError={isError}
emptyType="results"
emptyMessage={
isMe
? _(msg`No followers yet`)
: _(msg`This user doesn't have any followers.`)
}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
sideBorders={false}
useEmptyState={true}
emptyStateIcon={PeopleRemoveIcon}
emptyStateButton={{
label: _(msg`Go back`),
text: _(msg`Go back`),
color: 'secondary',
size: 'small',
onPress: () => navigation.goBack(),
}}
/>
)
}
const [followersPromoDismissed, setFollowersPromoDismissed] =
useFollowersPromoDismissed()
const findContactsEnabled =
useIsFindContactsFeatureEnabledBasedOnGeolocation()
// The banner deep-links into the Find and Invite Friends settings screen, so
// mirror that screen's availability gates: native-only, allowed in the user's
// region (geolocation allowlist), and not disabled by the feature flag. This
// avoids promoting contact import where the settings entry itself is hidden.
const showFollowersPromo =
IS_NATIVE &&
isMe &&
findContactsEnabled &&
!ax.features.enabled(ax.features.ImportContactsSettingsDisable) &&
!followersPromoDismissed &&
followers.length < 1 &&
!isDidLoading &&
!isFollowersLoading &&
!isError
return (
<List
data={followers}
renderItem={renderItemWithContext}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={onItemSeen}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
<>
{showFollowersPromo && (
<FollowersPromoBanner
onPress={() => navigation.navigate('FindContactsSettings')}
onDismiss={() => setFollowersPromoDismissed(true)}
/>
}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
sideBorders={false}
/>
)}
{followers.length < 1 ? (
<ListMaybePlaceholder
isLoading={isDidLoading || isFollowersLoading}
isError={isError}
emptyType="results"
emptyMessage={
isMe
? _(msg`No followers yet`)
: _(msg`This user doesn't have any followers.`)
}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
sideBorders={false}
useEmptyState={true}
emptyStateIcon={PeopleRemoveIcon}
emptyStateButton={{
label: _(msg`Go back`),
text: _(msg`Go back`),
color: 'secondary',
size: 'small',
onPress: () => navigation.goBack(),
}}
/>
) : (
<List
data={followers}
renderItem={renderItemWithContext}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={onItemSeen}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
sideBorders={false}
/>
)}
</>
)
}
+12
View File
@@ -7,6 +7,8 @@ import {useSetThemePrefs} from '#/state/shell'
import {ListContained} from '#/view/screens/Storybook/ListContained'
import {atoms as a, ThemeProvider} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {InviteFriendsDialog} from '#/features/inviteFriends'
import {
useDeviceGeolocationApi,
useRequestDeviceGeolocation,
@@ -33,6 +35,7 @@ export default function Storybook() {
const navigation = useNavigation<NavigationProp>()
const requestDeviceGeolocation = useRequestDeviceGeolocation()
const {setDeviceGeolocation} = useDeviceGeolocationApi()
const inviteFriendsControl = Dialog.useDialogControl()
return (
<>
@@ -98,6 +101,15 @@ export default function Storybook() {
<ButtonText>Get GPS Location</ButtonText>
</Button>
<Button
color="primary"
size="large"
onPress={() => inviteFriendsControl.open()}
label="Open invite friends sheet (APP-2142)">
<ButtonText>Open invite friends sheet (APP-2142)</ButtonText>
</Button>
<InviteFriendsDialog control={inviteFriendsControl} />
<ThemeProvider theme="light">
<Theming />
</ThemeProvider>
+59 -3
View File
@@ -1,5 +1,11 @@
import {type ComponentProps, type JSX, memo, useCallback} from 'react'
import {Linking, ScrollView, TouchableOpacity, View} from 'react-native'
import {
Linking,
Pressable,
ScrollView,
TouchableOpacity,
View,
} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
@@ -25,7 +31,9 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
import {NavSignupCard} from '#/view/shell/NavSignupCard'
import {atoms as a, tokens, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {Divider} from '#/components/Divider'
import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRight} from '#/components/icons/ArrowShareRight'
import {
Bell_Filled_Corner0_Rounded as BellFilled,
Bell_Stroke2_Corner0_Rounded as Bell,
@@ -57,7 +65,8 @@ import {InlineLinkText} from '#/components/Link'
import {ProfileBadges} from '#/components/ProfileBadges'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_WEB} from '#/env'
import {IS_NATIVE, IS_WEB} from '#/env'
import {InviteFriendsDialog} from '#/features/inviteFriends'
import {useActorStatus} from '#/features/liveNow'
const iconWidth = 26
@@ -65,9 +74,11 @@ const iconWidth = 26
let DrawerProfileCard = ({
account,
onPressProfile,
onPressShare,
}: {
account: SessionAccount
onPressProfile: () => void
onPressShare?: () => void
}): React.ReactNode => {
const {_, i18n} = useLingui()
const t = useTheme()
@@ -93,11 +104,45 @@ let DrawerProfileCard = ({
<View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}>
<Text
emoji
style={[a.font_bold, a.text_xl, a.mt_2xs, a.leading_tight]}
style={[
a.font_bold,
a.text_xl,
a.mt_2xs,
a.leading_tight,
a.flex_shrink,
]}
numberOfLines={1}>
{profile?.displayName || account.handle}
</Text>
{profile && <ProfileBadges profile={profile} size="lg" />}
{onPressShare && (
<Pressable
accessibilityRole="button"
accessibilityLabel={_(msg`Invite friends`)}
accessibilityHint={_(
msg`Opens the invite friends sheet to share your profile`,
)}
onPress={onPressShare}
hitSlop={8}
style={({pressed}) => [
a.ml_auto,
{
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: t.palette.contrast_50,
alignItems: 'center',
justifyContent: 'center',
opacity: pressed ? 0.7 : 1,
},
]}>
<ArrowShareRight
width={16}
height={16}
fill={t.palette.primary_500}
/>
</Pressable>
)}
</View>
<Text
emoji
@@ -151,6 +196,7 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
isAtMessages,
} = useNavigationTabState()
const {hasSession, currentAccount} = useSession()
const inviteFriendsControl = useDialogControl()
// events
// =
@@ -281,6 +327,15 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
<DrawerProfileCard
account={currentAccount}
onPressProfile={onPressDrawerHeaderProfile}
onPressShare={
IS_NATIVE
? () => {
ax.metric('invite:dialog:open', {logContext: 'Drawer'})
setDrawerOpen(false)
inviteFriendsControl.open()
}
: undefined
}
/>
) : (
<View style={[a.pr_xl]}>
@@ -330,6 +385,7 @@ let DrawerContent = ({}: React.PropsWithoutRef<{}>): React.ReactNode => {
onPressFeedback={onPressFeedback}
onPressHelp={onPressHelp}
/>
<InviteFriendsDialog control={inviteFriendsControl} />
</View>
)
}