[Contacts] Contacts matching flow (#9486)
* add expo-contacts * add expo-sms * update copy * add basic settings screen * state machine, flow screen * phone input screen * otp screen * tweak spacing * resend code logic * add layoutanimationconfig * check availablility in settings screen * get contacts * matches screen * search, temp setup for matches UI * add a bunch of number parsing logic with libphonenumber * cast to looser type * rename sync to find FCF (Find Contacts Flow) * update geolocation hook * nicer design for settings screen * add completed state * up border contrast * update expo deps * add pending spinner * add country whitelist * add empty state screen * drop add more functionality * upload -> import * fix typo * fix permission string * copy updates * rm envelope icon * update sms copy * add inviteinfo component * woke is back * [Contacts] NUXes (#9515) * add a bunch of number parsing logic with libphonenumber * add banner nux * add announcement nux * native only nux * rm shitty animation * move isNative check * [Contacts] Onboarding step (#9489) * add a bunch of number parsing logic with libphonenumber * restructure onboarding to better support dynamic screens * integrate existing flow into onboarding * add intro step * lift state up to allow going back freely * gate onboarding by geo if unsupported country * add done button to standalone flow * [Contacts] Add `contact-match` notification type to feed (#9519) * add a bunch of number parsing logic with libphonenumber * add contact-joined notif type * update string, api package * Update NotificationFeedItem.tsx * Update NotificationFeedItem.tsx * Delete SyncContactsFlow.web.tsx * fix follow back btn for this case * [Contacts] API integration (#9487) * api integration for flow * copy tweak * tweaks after running it * wire up status page * rename toast * use api lib * rm temp code * maybe fix otp error * clear code on error/resend * add 1s delay to verify success * update package versions * Delete SyncContactsFlow.web.tsx * try and fix yarn.lock lint * even woker * fix uppercase friends * surfdude feedback Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * rm log * allow resend on invalid code * Update GetContacts.tsx * overwrite country code if possible * fix Apple's Best Feature * devenv latest * disable bounces * interactive keyboard dismiss * copy changes * national format * allow resending immediately * move success state down a bit * Update FindContactsSettings.tsx * [Contacts] More onboarding changes (#9491) * integrate existing flow into onboarding * refreshed onboarding styles, rm stepper * center content on web * Add back dismiss button for internal onboarding * Import sort --------- Co-authored-by: Eric Bailey <git@esb.lol> * add matches query to shadow * add clockwise arrow * update status design, fix dismiss, fix queries * add metrics * add more comments * Update metrics.ts * refetch both queries on PTR * fix shadow state in matches page * reduce empty space at bottom * show contact info on matches * filter out contacts without numbers at an earlier stage * Error handling ✨ * get notifs working * filter out businesses * rm log * remove TODO from learn more links * try and exclude from web bundle --------- Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
@@ -29,7 +29,7 @@ import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
|
||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||
|
||||
export function GifSelectDialog({
|
||||
|
||||
@@ -25,7 +25,7 @@ import {Button, ButtonIcon} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {canBeMessaged} from '#/components/dms/util'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {MagnifyingGlass_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import {useCallback} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {LinearGradient} from 'expo-linear-gradient'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useNuxDialogContext} from '#/components/dialogs/nuxs'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {navigate} from '#/Navigation'
|
||||
|
||||
export function FindContactsAnnouncement() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const nuxDialogs = useNuxDialogContext()
|
||||
const control = Dialog.useDialogControl()
|
||||
|
||||
Dialog.useAutoOpen(control)
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
nuxDialogs.dismissActiveNux()
|
||||
}, [nuxDialogs])
|
||||
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
onClose={onClose}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.ScrollableInner
|
||||
label={_(msg`Introducing finding friends via contacts`)}
|
||||
style={[web({maxWidth: 440})]}
|
||||
contentContainerStyle={[
|
||||
{
|
||||
paddingTop: 0,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
},
|
||||
]}>
|
||||
<View style={[a.align_center, a.pt_3xl]}>
|
||||
<LinearGradient
|
||||
colors={[t.palette.primary_200, t.atoms.bg.backgroundColor]}
|
||||
locations={[0, 1]}
|
||||
start={{x: 0, y: 0}}
|
||||
end={{x: 0, y: 1}}
|
||||
style={[a.absolute, a.inset_0]}
|
||||
/>
|
||||
<View style={[a.w_full, a.pt_sm, a.px_5xl, a.pb_4xl]}>
|
||||
<Image
|
||||
accessibilityIgnoresInvertColors
|
||||
source={require('../../../../assets/images/find_friends_illustration.webp')}
|
||||
style={[a.w_full, {aspectRatio: 1278 / 661}]}
|
||||
alt={_(
|
||||
msg`An illustration depicting user avatars flowing from a contact book into the Bluesky app`,
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={[a.align_center, a.px_xl, a.gap_5xl]}>
|
||||
<View style={[a.gap_sm, a.align_center]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_4xl,
|
||||
a.leading_tight,
|
||||
a.font_bold,
|
||||
a.text_center,
|
||||
{
|
||||
fontSize: isWeb ? 28 : 32,
|
||||
maxWidth: 300,
|
||||
},
|
||||
]}>
|
||||
<Trans>Find your friends</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.leading_snug,
|
||||
a.text_center,
|
||||
{maxWidth: 340},
|
||||
]}>
|
||||
<Trans>
|
||||
Bluesky is more fun with friends! Import your contacts to see
|
||||
who’s already here.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Button
|
||||
label={_(msg`Import Contacts`)}
|
||||
size="large"
|
||||
color="primary"
|
||||
onPress={() => {
|
||||
logger.metric('contacts:nux:ctaPressed', {})
|
||||
control.close(() => {
|
||||
navigate('FindContactsFlow')
|
||||
})
|
||||
}}
|
||||
style={[a.w_full]}>
|
||||
<ButtonText>
|
||||
<Trans>Import Contacts</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {type AppBskyActorDefs} from '@atproto/api'
|
||||
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {STALE} from '#/state/queries'
|
||||
import {Nux, useNuxs, useResetNuxs, useSaveNux} from '#/state/queries/nuxs'
|
||||
import {
|
||||
@@ -12,11 +20,11 @@ import {
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {type SessionAccount, useSession} from '#/state/session'
|
||||
import {useOnboardingState} from '#/state/shell'
|
||||
import {BookmarksAnnouncement} from '#/components/dialogs/nuxs/BookmarksAnnouncement'
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
/*
|
||||
* NUXs
|
||||
*/
|
||||
import {isSnoozed, snooze, unsnooze} from '#/components/dialogs/nuxs/snoozing'
|
||||
import {FindContactsAnnouncement} from './FindContactsAnnouncement'
|
||||
import {isExistingUserAsOf} from './utils'
|
||||
|
||||
type Context = {
|
||||
@@ -34,24 +42,24 @@ const queuedNuxs: {
|
||||
}) => boolean
|
||||
}[] = [
|
||||
{
|
||||
id: Nux.BookmarksAnnouncement,
|
||||
id: Nux.FindContactsAnnouncement,
|
||||
enabled: ({currentProfile}) => {
|
||||
return isExistingUserAsOf(
|
||||
'2025-09-08T00:00:00.000Z',
|
||||
currentProfile.createdAt,
|
||||
return (
|
||||
isNative &&
|
||||
isExistingUserAsOf('2025-12-16T00:00:00.000Z', currentProfile.createdAt)
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const Context = React.createContext<Context>({
|
||||
const Context = createContext<Context>({
|
||||
activeNux: undefined,
|
||||
dismissActiveNux: () => {},
|
||||
})
|
||||
Context.displayName = 'NuxDialogContext'
|
||||
|
||||
export function useNuxDialogContext() {
|
||||
return React.useContext(Context)
|
||||
return useContext(Context)
|
||||
}
|
||||
|
||||
export function NuxDialogs() {
|
||||
@@ -92,19 +100,19 @@ function Inner({
|
||||
}) {
|
||||
const gate = useGate()
|
||||
const {nuxs} = useNuxs()
|
||||
const [snoozed, setSnoozed] = React.useState(() => {
|
||||
const [snoozed, setSnoozed] = useState(() => {
|
||||
return isSnoozed()
|
||||
})
|
||||
const [activeNux, setActiveNux] = React.useState<Nux | undefined>()
|
||||
const [activeNux, setActiveNux] = useState<Nux | undefined>()
|
||||
const {mutateAsync: saveNux} = useSaveNux()
|
||||
const {mutate: resetNuxs} = useResetNuxs()
|
||||
|
||||
const snoozeNuxDialog = React.useCallback(() => {
|
||||
const snoozeNuxDialog = useCallback(() => {
|
||||
snooze()
|
||||
setSnoozed(true)
|
||||
}, [setSnoozed])
|
||||
|
||||
const dismissActiveNux = React.useCallback(() => {
|
||||
const dismissActiveNux = useCallback(() => {
|
||||
if (!activeNux) return
|
||||
setActiveNux(undefined)
|
||||
}, [activeNux, setActiveNux])
|
||||
@@ -118,7 +126,7 @@ function Inner({
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (snoozed) return // comment this out to test
|
||||
if (!nuxs) return
|
||||
|
||||
@@ -170,7 +178,7 @@ function Inner({
|
||||
preferences,
|
||||
])
|
||||
|
||||
const ctx = React.useMemo(() => {
|
||||
const ctx = useMemo(() => {
|
||||
return {
|
||||
activeNux,
|
||||
dismissActiveNux,
|
||||
@@ -180,7 +188,9 @@ function Inner({
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
{/*For example, activeNux === Nux.NeueTypography && <NeueTypography />*/}
|
||||
{activeNux === Nux.BookmarksAnnouncement && <BookmarksAnnouncement />}
|
||||
{activeNux === Nux.FindContactsAnnouncement && (
|
||||
<FindContactsAnnouncement />
|
||||
)}
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user