[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:
Samuel Newman
2025-12-12 20:32:06 +02:00
committed by GitHub
parent d3a6db4594
commit ad7d49ef8f
98 changed files with 4906 additions and 1079 deletions
@@ -1,11 +1,4 @@
import {
memo,
type ReactElement,
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import {memo, useCallback, useEffect, useMemo, useState} from 'react'
import {
Animated,
type GestureResponderEvent,
@@ -61,6 +54,7 @@ import {
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
} from '#/components/icons/Chevron'
import {Contacts_Filled_Corner2_Rounded as ContactsIconFilled} from '#/components/icons/Contacts'
import {Heart2_Filled_Stroke2_Corner0_Rounded as HeartIconFilled} from '#/components/icons/Heart2'
import {PersonPlus_Filled_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
@@ -116,6 +110,7 @@ let NotificationFeedItem = ({
break
}
case 'follow':
case 'contact-match':
case 'verified':
case 'unverified': {
return makeProfileLink(item.notification.author)
@@ -279,7 +274,7 @@ let NotificationFeedItem = ({
: ''
let a11yLabel = ''
let notificationContent: ReactElement<any>
let notificationContent: React.ReactElement<any>
let icon = (
<HeartIconFilled
size="xl"
@@ -373,6 +368,14 @@ let NotificationFeedItem = ({
)
}
icon = <PersonPlusIcon size="xl" style={{color: t.palette.primary_500}} />
} else if (item.type === 'contact-match') {
a11yLabel = _(msg`Your contact ${firstAuthorName} is on Bluesky`)
notificationContent = (
<Trans>Your contact {firstAuthorLink} is on Bluesky</Trans>
)
icon = (
<ContactsIconFilled size="xl" style={{color: t.palette.primary_500}} />
)
} else if (item.type === 'feedgen-like') {
a11yLabel = hasMultipleAuthors
? _(
@@ -438,7 +441,7 @@ let NotificationFeedItem = ({
notificationContent = hasMultipleAuthors ? (
<Trans>
{firstAuthorLink} and{' '}
<Text style={[pal.text, s.bold]}>
<Text style={[a.text_md, a.font_semi_bold, a.leading_snug]}>
<Plural
value={additionalAuthorsCount}
one={`${formattedAuthorsCount} other`}
@@ -463,7 +466,7 @@ let NotificationFeedItem = ({
notificationContent = hasMultipleAuthors ? (
<Trans>
{firstAuthorLink} and{' '}
<Text style={[pal.text, s.bold]}>
<Text style={[a.text_md, a.font_semi_bold, a.leading_snug]}>
<Plural
value={additionalAuthorsCount}
one={`${formattedAuthorsCount} other`}
@@ -670,7 +673,9 @@ let NotificationFeedItem = ({
</TimeElapsed>
</Text>
</ExpandListPressable>
{item.type === 'follow' && !hasMultipleAuthors && !isFollowBack ? (
{(item.type === 'follow' && !hasMultipleAuthors && !isFollowBack) ||
(item.type === 'contact-match' &&
!item.notification.author.viewer?.following) ? (
<FollowBackButton profile={item.notification.author} />
) : null}
{item.type === 'post-like' ||
@@ -809,6 +814,7 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) {
}
const isFollowing = profileShadow.viewer.following
const isFollowedBy = profileShadow.viewer.followedBy
const followingLabel = _(
msg({
message: 'Following',
@@ -832,14 +838,14 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) {
</Button>
) : (
<Button
label={_(msg`Follow back`)}
label={isFollowedBy ? _(msg`Follow back`) : _(msg`Follow`)}
color="primary"
size="small"
style={[a.self_start]}
onPress={onPressFollow}>
<ButtonIcon icon={PlusIcon} />
<ButtonText>
<Trans>Follow back</Trans>
{isFollowedBy ? <Trans>Follow back</Trans> : <Trans>Follow</Trans>}
</ButtonText>
</Button>
)}
+2
View File
@@ -12,6 +12,7 @@ import {isWeb} from '#/platform/detection'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {useSession} from '#/state/session'
import {FindContactsBannerNUX} from '#/components/contacts/FindContactsBannerNUX'
import {PeopleRemove2_Stroke1_Corner0_Rounded as PeopleRemoveIcon} from '#/components/icons/PeopleRemove2'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {List} from '../util/List'
@@ -208,6 +209,7 @@ export function ProfileFollows({name}: {name: string}) {
onEndReached={onEndReached}
onEndReachedThreshold={4}
onItemSeen={onItemSeen}
ListHeaderComponent={<FindContactsBannerNUX />}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
+1 -1
View File
@@ -37,7 +37,7 @@ import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/Do
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
import {Live_Stroke2_Corner0_Rounded as LiveIcon} from '#/components/icons/Live'
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass2'
import {MagnifyingGlass_Stroke2_Corner0_Rounded as SearchIcon} from '#/components/icons/MagnifyingGlass'
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
import {
+1 -1
View File
@@ -9,7 +9,7 @@ import {useLingui} from '@lingui/react'
import {usePalette} from '#/lib/hooks/usePalette'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotateCounterClockwise'
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as ArrowRotateCounterClockwiseIcon} from '#/components/icons/ArrowRotate'
import * as Layout from '#/components/Layout'
import {Text} from '#/components/Typography'