···01import {Alert, View} from 'react-native'
2import {useSafeAreaInsets} from 'react-native-safe-area-context'
3import * as Contacts from 'expo-contacts'
4-import {AppBskyContactImportContacts} from '@atproto/api'
000005import {msg, t, Trans} from '@lingui/macro'
6import {useLingui} from '@lingui/react'
7import {useMutation, useQueryClient} from '@tanstack/react-query'
809import {cleanError, isNetworkError} from '#/lib/strings/errors'
10import {logger} from '#/logger'
11import {findContactsStatusQueryKey} from '#/state/queries/find-contacts'
12import {useAgent} from '#/state/session'
0000013import {atoms as a, ios, tokens, useGutters} from '#/alf'
14import {Button, ButtonIcon, ButtonText} from '#/components/Button'
15import * as Layout from '#/components/Layout'
···43 const insets = useSafeAreaInsets()
44 const gutters = useGutters([0, 'wide'])
45 const queryClient = useQueryClient()
04647 const {mutate: uploadContacts, isPending: isUploadPending} = useMutation({
48 mutationFn: async (contacts: Contacts.ExistingContact[]) => {
000000000000000049 const {phoneNumbers, indexToContactId} = normalizeContactBook(
50 contacts,
51 state.phoneCountryCode,
···202 <Text style={style}>
203 <Trans>
204 Bluesky helps friends find each other by creating an encoded digital
205- fingerprint, called a "hash," and then looking for matching hashes.
206 </Trans>
207 </Text>
208 <Text style={style}>
209- • <Trans>We never store plain phone numbers</Trans>
210 </Text>
211 <Text style={style}>
212 • <Trans>We delete hashes after matches are made</Trans>
···288 ],
289 )
290}
00000000000000000000000000000000
···1+import {useContext} from 'react'
2import {Alert, View} from 'react-native'
3import {useSafeAreaInsets} from 'react-native-safe-area-context'
4import * as Contacts from 'expo-contacts'
5+import type AtpAgent from '@atproto/api'
6+import {
7+ type AppBskyActorProfile,
8+ AppBskyContactImportContacts,
9+ type Un$Typed,
10+} from '@atproto/api'
11import {msg, t, Trans} from '@lingui/macro'
12import {useLingui} from '@lingui/react'
13import {useMutation, useQueryClient} from '@tanstack/react-query'
1415+import {uploadBlob} from '#/lib/api'
16import {cleanError, isNetworkError} from '#/lib/strings/errors'
17import {logger} from '#/logger'
18import {findContactsStatusQueryKey} from '#/state/queries/find-contacts'
19import {useAgent} from '#/state/session'
20+import {
21+ Context as OnboardingContext,
22+ type OnboardingAction,
23+ type OnboardingState,
24+} from '#/screens/Onboarding/state'
25import {atoms as a, ios, tokens, useGutters} from '#/alf'
26import {Button, ButtonIcon, ButtonText} from '#/components/Button'
27import * as Layout from '#/components/Layout'
···55 const insets = useSafeAreaInsets()
56 const gutters = useGutters([0, 'wide'])
57 const queryClient = useQueryClient()
58+ const maybeOnboardingContext = useContext(OnboardingContext)
5960 const {mutate: uploadContacts, isPending: isUploadPending} = useMutation({
61 mutationFn: async (contacts: Contacts.ExistingContact[]) => {
62+ /**
63+ * `importContacts` triggers a notification for the people you match with,
64+ * however we prevent notifications coming from users without profiles.
65+ * If you're using this as the onboarding flow, we need to create a profile
66+ * record before this.
67+ *
68+ * When you finish onboarding, we'll upsert again - bit wasteful but fine.
69+ */
70+ if (context === 'Onboarding' && maybeOnboardingContext) {
71+ try {
72+ await createProfileRecord(agent, maybeOnboardingContext)
73+ } catch (error) {
74+ logger.debug('Error creating profile record:', {safeMessage: error})
75+ }
76+ }
77+78 const {phoneNumbers, indexToContactId} = normalizeContactBook(
79 contacts,
80 state.phoneCountryCode,
···231 <Text style={style}>
232 <Trans>
233 Bluesky helps friends find each other by creating an encoded digital
234+ fingerprint, called a "hash", and then looking for matching hashes.
235 </Trans>
236 </Text>
237 <Text style={style}>
238+ • <Trans>We never keep plain phone numbers</Trans>
239 </Text>
240 <Text style={style}>
241 • <Trans>We delete hashes after matches are made</Trans>
···317 ],
318 )
319}
320+321+/**
322+ * Copied from `#/screens/Onboarding/StepFinished/index.tsx`
323+ */
324+async function createProfileRecord(
325+ agent: AtpAgent,
326+ onboardingContext: {
327+ state: OnboardingState
328+ dispatch: React.Dispatch<OnboardingAction>
329+ },
330+) {
331+ const profileStepResults = onboardingContext.state.profileStepResults
332+ const {imageUri, imageMime} = profileStepResults
333+ const blobPromise =
334+ imageUri && imageMime ? uploadBlob(agent, imageUri, imageMime) : undefined
335+336+ await agent.upsertProfile(async existing => {
337+ let next: Un$Typed<AppBskyActorProfile.Record> = existing ?? {}
338+339+ if (blobPromise) {
340+ const res = await blobPromise
341+ if (res.data.blob) {
342+ next.avatar = res.data.blob
343+ }
344+ }
345+346+ next.displayName = ''
347+348+ next.createdAt = new Date().toISOString()
349+ return next
350+ })
351+}
+4-5
src/screens/Onboarding/StepFinished/index.tsx
···161 }
162163 next.displayName = ''
164- // HACKFIX
165- // creating a bunch of identical profile objects is breaking the relay
166- // tossing this unspecced field onto it to reduce the size of the problem
167- // -prf
168- next.createdAt = new Date().toISOString()
169 return next
170 })
171
···161 }
162163 next.displayName = ''
164+165+ if (!next.createdAt) {
166+ next.createdAt = new Date().toISOString()
167+ }
0168 return next
169 })
170