Add profile QR code for sharing and inviting friends (#10659)
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user