[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:
@@ -0,0 +1,562 @@
|
||||
import {useCallback, useEffect, useState} from 'react'
|
||||
import {type ListRenderItemInfo, View} from 'react-native'
|
||||
import * as Contacts from 'expo-contacts'
|
||||
import {
|
||||
type AppBskyContactDefs,
|
||||
type AppBskyContactGetSyncStatus,
|
||||
type ModerationOpts,
|
||||
} from '@atproto/api'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useIsFocused} from '@react-navigation/native'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted'
|
||||
import {
|
||||
type AllNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {cleanError, isNetworkError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {
|
||||
updateProfileShadow,
|
||||
useProfileShadow,
|
||||
} from '#/state/cache/profile-shadow'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {
|
||||
findContactsStatusQueryKey,
|
||||
optimisticRemoveMatch,
|
||||
useContactsMatchesQuery,
|
||||
useContactsSyncStatusQuery,
|
||||
} from '#/state/queries/find-contacts'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {List} from '#/view/com/util/List'
|
||||
import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {ContactsHeroImage} from '#/components/contacts/components/HeroImage'
|
||||
import {ArrowRotateClockwise_Stroke2_Corner0_Rounded as ResyncIcon} from '#/components/icons/ArrowRotate'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import {InlineLinkText, Link} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as ProfileCard from '#/components/ProfileCard'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {Text} from '#/components/Typography'
|
||||
import type * as bsky from '#/types/bsky'
|
||||
import {bulkWriteFollows} from '../Onboarding/util'
|
||||
|
||||
type Props = NativeStackScreenProps<AllNavigatorParams, 'FindContactsSettings'>
|
||||
export function FindContactsSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
|
||||
const {data, error, refetch} = useContactsSyncStatusQuery()
|
||||
|
||||
const isFocused = useIsFocused()
|
||||
useEffect(() => {
|
||||
if (data && isFocused) {
|
||||
logger.metric('contacts:settings:presented', {
|
||||
hasPreviouslySynced: !!data.syncStatus,
|
||||
matchCount: data.syncStatus?.matchesCount,
|
||||
})
|
||||
}
|
||||
}, [data, isFocused])
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
<Layout.Header.Outer>
|
||||
<Layout.Header.BackButton />
|
||||
<Layout.Header.Content>
|
||||
<Layout.Header.TitleText>
|
||||
<Trans>Find Friends</Trans>
|
||||
</Layout.Header.TitleText>
|
||||
</Layout.Header.Content>
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
{isNative ? (
|
||||
data ? (
|
||||
!data.syncStatus ? (
|
||||
<Intro />
|
||||
) : (
|
||||
<SyncStatus info={data.syncStatus} refetchStatus={refetch} />
|
||||
)
|
||||
) : error ? (
|
||||
<ErrorScreen
|
||||
title={_(msg`Error getting the latest data.`)}
|
||||
message={cleanError(error)}
|
||||
onPressTryAgain={refetch}
|
||||
/>
|
||||
) : (
|
||||
<View style={[a.flex_1, a.justify_center, a.align_center]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
<ErrorScreen
|
||||
title={_(msg`Not available on this platform.`)}
|
||||
message={_(msg`Please use the native app to sync your contacts.`)}
|
||||
/>
|
||||
)}
|
||||
</Layout.Screen>
|
||||
)
|
||||
}
|
||||
|
||||
function Intro() {
|
||||
const gutter = useGutters(['base'])
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const {data: isAvailable, isSuccess} = useQuery({
|
||||
queryKey: ['contacts-available'],
|
||||
queryFn: async () => await Contacts.isAvailableAsync(),
|
||||
})
|
||||
|
||||
return (
|
||||
<Layout.Content contentContainerStyle={[gutter, a.gap_lg]}>
|
||||
<ContactsHeroImage />
|
||||
<Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Find your friends on Bluesky by verifying your phone number and
|
||||
matching with your contacts. We protect your information and you
|
||||
control what happens next.{' '}
|
||||
<InlineLinkText
|
||||
to="#"
|
||||
label={_(
|
||||
msg({
|
||||
message: `Learn more about importing contacts`,
|
||||
context: `english-only-resource`,
|
||||
}),
|
||||
)}
|
||||
style={[a.text_md, a.leading_snug]}>
|
||||
<Trans context="english-only-resource">Learn more</Trans>
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
</Text>
|
||||
{isAvailable ? (
|
||||
<Link
|
||||
to={{screen: 'FindContactsFlow'}}
|
||||
label={_(msg`Import contacts`)}
|
||||
size="large"
|
||||
color="primary"
|
||||
style={[a.flex_1, a.justify_center]}>
|
||||
<ButtonText>
|
||||
<Trans>Import contacts</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
) : (
|
||||
isSuccess && (
|
||||
<Admonition type="error">
|
||||
<Trans>
|
||||
Contact sync is not available on this device, as the app is unable
|
||||
to access your contacts.
|
||||
</Trans>
|
||||
</Admonition>
|
||||
)
|
||||
)}
|
||||
</Layout.Content>
|
||||
)
|
||||
}
|
||||
|
||||
function SyncStatus({
|
||||
info,
|
||||
refetchStatus,
|
||||
}: {
|
||||
info: AppBskyContactDefs.SyncStatus
|
||||
refetchStatus: () => Promise<any>
|
||||
}) {
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const {_} = useLingui()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const {
|
||||
data,
|
||||
isPending,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
refetch: refetchMatches,
|
||||
} = useContactsMatchesQuery()
|
||||
|
||||
const [isPTR, setIsPTR] = useState(false)
|
||||
|
||||
const onRefresh = () => {
|
||||
setIsPTR(true)
|
||||
Promise.all([refetchStatus(), refetchMatches()]).finally(() => {
|
||||
setIsPTR(false)
|
||||
})
|
||||
}
|
||||
|
||||
const {mutate: dismissMatch} = useMutation({
|
||||
mutationFn: async (did: string) => {
|
||||
await agent.app.bsky.contact.dismissMatch({subject: did})
|
||||
},
|
||||
onMutate: async (did: string) => {
|
||||
logger.metric('contacts:settings:dismiss', {})
|
||||
optimisticRemoveMatch(queryClient, did)
|
||||
},
|
||||
onError: err => {
|
||||
refetchMatches()
|
||||
if (isNetworkError(err)) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`Could not follow all matches - please check your network connection.`,
|
||||
),
|
||||
{type: 'error'},
|
||||
)
|
||||
} else {
|
||||
logger.error('Failed to follow all matches', {safeMessage: err})
|
||||
Toast.show(_(msg`Could not follow all matches. ${cleanError(err)}`), {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const profiles = data?.pages?.flatMap(page => page.matches) ?? []
|
||||
|
||||
const numProfiles = profiles.length
|
||||
const isAnyUnfollowed = profiles.some(profile => !profile.viewer?.following)
|
||||
|
||||
const renderItem = useCallback(
|
||||
({item, index}: ListRenderItemInfo<bsky.profile.AnyProfileView>) => {
|
||||
if (!moderationOpts) return null
|
||||
return (
|
||||
<MatchItem
|
||||
profile={item}
|
||||
isFirst={index === 0}
|
||||
isLast={index === numProfiles - 1}
|
||||
moderationOpts={moderationOpts}
|
||||
dismissMatch={dismissMatch}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[numProfiles, moderationOpts, dismissMatch],
|
||||
)
|
||||
|
||||
const onEndReached = () => {
|
||||
if (!hasNextPage || isFetchingNextPage) return
|
||||
fetchNextPage()
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
data={profiles}
|
||||
renderItem={renderItem}
|
||||
ListHeaderComponent={
|
||||
<StatusHeader
|
||||
numMatches={info.matchesCount}
|
||||
isPending={isPending}
|
||||
isAnyUnfollowed={isAnyUnfollowed}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={<StatusFooter syncedAt={info.syncedAt} />}
|
||||
onRefresh={onRefresh}
|
||||
refreshing={isPTR}
|
||||
onEndReached={onEndReached}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MatchItem({
|
||||
profile,
|
||||
isFirst,
|
||||
isLast,
|
||||
moderationOpts,
|
||||
dismissMatch,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
isFirst: boolean
|
||||
isLast: boolean
|
||||
moderationOpts: ModerationOpts
|
||||
dismissMatch: (did: string) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const shadow = useProfileShadow(profile)
|
||||
|
||||
return (
|
||||
<View style={[a.px_xl]}>
|
||||
<View
|
||||
style={[
|
||||
a.p_md,
|
||||
a.border_t,
|
||||
a.border_x,
|
||||
t.atoms.border_contrast_high,
|
||||
isFirst && [
|
||||
a.curve_continuous,
|
||||
{borderTopLeftRadius: tokens.borderRadius.lg},
|
||||
{borderTopRightRadius: tokens.borderRadius.lg},
|
||||
],
|
||||
isLast && [
|
||||
a.border_b,
|
||||
a.curve_continuous,
|
||||
{borderBottomLeftRadius: tokens.borderRadius.lg},
|
||||
{borderBottomRightRadius: tokens.borderRadius.lg},
|
||||
a.mb_sm,
|
||||
],
|
||||
]}>
|
||||
<ProfileCard.Header>
|
||||
<ProfileCard.Avatar
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.NameAndHandle
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
/>
|
||||
<ProfileCard.FollowButton
|
||||
profile={profile}
|
||||
moderationOpts={moderationOpts}
|
||||
logContext="FindContacts"
|
||||
onFollow={() => logger.metric('contacts:settings:follow', {})}
|
||||
/>
|
||||
{!shadow.viewer?.following && (
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
label={_(msg`Remove suggestion`)}
|
||||
onPress={() => dismissMatch(profile.did)}
|
||||
hoverStyle={[a.bg_transparent, {opacity: 0.5}]}
|
||||
hitSlop={8}>
|
||||
<ButtonIcon icon={XIcon} />
|
||||
</Button>
|
||||
)}
|
||||
</ProfileCard.Header>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusHeader({
|
||||
numMatches,
|
||||
isPending,
|
||||
isAnyUnfollowed,
|
||||
}: {
|
||||
numMatches: number
|
||||
isPending: boolean
|
||||
isAnyUnfollowed: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
const {currentAccount} = useSession()
|
||||
|
||||
const {
|
||||
mutate: onFollowAll,
|
||||
isPending: isFollowingAll,
|
||||
isSuccess: hasFollowedAll,
|
||||
} = useMutation({
|
||||
mutationFn: async () => {
|
||||
const didsToFollow = []
|
||||
|
||||
let cursor: string | undefined
|
||||
do {
|
||||
const page = await agent.app.bsky.contact.getMatches({
|
||||
limit: 100,
|
||||
cursor,
|
||||
})
|
||||
cursor = page.data.cursor
|
||||
for (const profile of page.data.matches) {
|
||||
if (
|
||||
profile.did !== currentAccount?.did &&
|
||||
!isBlockedOrBlocking(profile) &&
|
||||
!isMuted(profile) &&
|
||||
!profile.viewer?.following
|
||||
) {
|
||||
didsToFollow.push(profile.did)
|
||||
}
|
||||
}
|
||||
} while (cursor)
|
||||
|
||||
logger.metric('contacts:settings:followAll', {
|
||||
followCount: didsToFollow.length,
|
||||
})
|
||||
|
||||
const uris = await wait(500, bulkWriteFollows(agent, didsToFollow))
|
||||
|
||||
for (const did of didsToFollow) {
|
||||
const uri = uris.get(did)
|
||||
updateProfileShadow(queryClient, did, {
|
||||
followingUri: uri,
|
||||
})
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
Toast.show(_(msg`Followed all matches`), {type: 'success'})
|
||||
},
|
||||
onError: err => {
|
||||
if (isNetworkError(err)) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`Could not follow all matches - please check your network connection.`,
|
||||
),
|
||||
{type: 'error'},
|
||||
)
|
||||
} else {
|
||||
logger.error('Failed to follow all matches', {safeMessage: err})
|
||||
Toast.show(_(msg`Could not follow all matches. ${cleanError(err)}`), {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (numMatches > 0) {
|
||||
if (isPending) {
|
||||
return (
|
||||
<View style={[a.w_full, a.py_3xl, a.align_center]}>
|
||||
<Loader size="xl" />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.pt_xl,
|
||||
a.px_xl,
|
||||
a.pb_md,
|
||||
a.flex_row,
|
||||
a.justify_between,
|
||||
a.align_center,
|
||||
]}>
|
||||
<Text style={[a.text_md, a.font_semi_bold]}>
|
||||
<Plural
|
||||
value={numMatches}
|
||||
one="1 contact found"
|
||||
other="# contacts found"
|
||||
/>
|
||||
</Text>
|
||||
{isAnyUnfollowed && (
|
||||
<Button
|
||||
label={_(msg`Follow all`)}
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onPress={() => onFollowAll()}
|
||||
disabled={isFollowingAll || hasFollowedAll}
|
||||
hitSlop={HITSLOP_10}
|
||||
style={[a.px_0, a.py_0, a.rounded_0]}
|
||||
hoverStyle={[a.bg_transparent, {opacity: 0.5}]}>
|
||||
<ButtonText>
|
||||
<Trans>Follow all</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function StatusFooter({syncedAt}: {syncedAt: string}) {
|
||||
const {_, i18n} = useLingui()
|
||||
const t = useTheme()
|
||||
const agent = useAgent()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const {mutate: removeData, isPending} = useMutation({
|
||||
mutationFn: async () => {
|
||||
await agent.app.bsky.contact.removeData({})
|
||||
},
|
||||
onMutate: () => logger.metric('contacts:settings:removeData', {}),
|
||||
onSuccess: () => {
|
||||
Toast.show(_(msg`Contacts removed`))
|
||||
queryClient.setQueryData<AppBskyContactGetSyncStatus.OutputSchema>(
|
||||
findContactsStatusQueryKey,
|
||||
{syncStatus: undefined},
|
||||
)
|
||||
},
|
||||
onError: err => {
|
||||
if (isNetworkError(err)) {
|
||||
Toast.show(
|
||||
_(
|
||||
msg`Failed to remove data due to a network error, please check your internet connection.`,
|
||||
),
|
||||
{type: 'error'},
|
||||
)
|
||||
} else {
|
||||
logger.error('Remove data failed', {safeMessage: err})
|
||||
Toast.show(_(msg`Failed to remove data. ${cleanError(err)}`), {
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={[a.px_xl, a.py_xl, a.gap_4xl]}>
|
||||
<View style={[a.gap_xs, a.align_start]}>
|
||||
<Text style={[a.text_md, a.font_semi_bold]}>
|
||||
<Trans>Contacts uploaded</Trans>
|
||||
</Text>
|
||||
<View style={[a.gap_2xs]}>
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>We will notify you when we find your friends.</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Uploaded on{' '}
|
||||
{i18n.date(new Date(syncedAt), {
|
||||
dateStyle: 'long',
|
||||
})}
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
<Link
|
||||
label={_(msg`Resync contacts`)}
|
||||
to={{screen: 'FindContactsFlow'}}
|
||||
onPress={() => {
|
||||
const daysSinceLastSync = Math.floor(
|
||||
(Date.now() - new Date(syncedAt).getTime()) /
|
||||
(1000 * 60 * 60 * 24),
|
||||
)
|
||||
logger.metric('contacts:settings:resync', {
|
||||
daysSinceLastSync,
|
||||
})
|
||||
}}
|
||||
size="small"
|
||||
color="primary_subtle"
|
||||
style={[a.mt_xs]}>
|
||||
<ButtonIcon icon={ResyncIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Resync contacts</Trans>
|
||||
</ButtonText>
|
||||
</Link>
|
||||
</View>
|
||||
|
||||
<View style={[a.gap_xs, a.align_start]}>
|
||||
<Text style={[a.text_md, a.font_semi_bold]}>
|
||||
<Trans>Delete contacts</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>
|
||||
Bluesky stores your contacts as encoded data. Removing your contacts
|
||||
will immediately delete this data.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Remove all contacts`)}
|
||||
onPress={() => removeData()}
|
||||
size="small"
|
||||
color="negative_subtle"
|
||||
disabled={isPending}
|
||||
style={[a.mt_xs]}>
|
||||
<ButtonIcon icon={isPending ? Loader : TrashIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Remove all contacts</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import {atoms as a, platform, tokens, useBreakpoints, useTheme} from '#/alf'
|
||||
import {AgeAssuranceDismissibleNotice} from '#/components/ageAssurance/AgeAssuranceDismissibleNotice'
|
||||
import {AvatarStackWithFetch} from '#/components/AvatarStack'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {useIsFindContactsFeatureEnabledBasedOnGeolocation} from '#/components/contacts/country-allowlist'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {SwitchAccountDialog} from '#/components/dialogs/SwitchAccount'
|
||||
import {Accessibility_Stroke2_Corner2_Rounded as AccessibilityIcon} from '#/components/icons/Accessibility'
|
||||
@@ -45,6 +46,7 @@ import {BubbleInfo_Stroke2_Corner2_Rounded as BubbleInfoIcon} from '#/components
|
||||
import {ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon} from '#/components/icons/Chevron'
|
||||
import {CircleQuestion_Stroke2_Corner2_Rounded as CircleQuestionIcon} from '#/components/icons/CircleQuestion'
|
||||
import {CodeBrackets_Stroke2_Corner2_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets'
|
||||
import {Contacts_Stroke2_Corner2_Rounded as ContactsIcon} from '#/components/icons/Contacts'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||
import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe'
|
||||
import {Lock_Stroke2_Corner2_Rounded as LockIcon} from '#/components/icons/Lock'
|
||||
@@ -89,6 +91,8 @@ export function SettingsScreen({}: Props) {
|
||||
const {pendingDid, onPressSwitchAccount} = useAccountSwitcher()
|
||||
const [showAccounts, setShowAccounts] = useState(false)
|
||||
const [showDevOptions, setShowDevOptions] = useState(false)
|
||||
const findContactsEnabled =
|
||||
useIsFindContactsFeatureEnabledBasedOnGeolocation()
|
||||
|
||||
return (
|
||||
<Layout.Screen>
|
||||
@@ -207,6 +211,16 @@ export function SettingsScreen({}: Props) {
|
||||
<Trans>Content and media</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
{isNative && findContactsEnabled && (
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/find-contacts"
|
||||
label={_(msg`Find friends from contacts`)}>
|
||||
<SettingsList.ItemIcon icon={ContactsIcon} />
|
||||
<SettingsList.ItemText>
|
||||
<Trans>Find friends from contacts</Trans>
|
||||
</SettingsList.ItemText>
|
||||
</SettingsList.LinkItem>
|
||||
)}
|
||||
<SettingsList.LinkItem
|
||||
to="/settings/appearance"
|
||||
label={_(msg`Appearance`)}>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {useMutation, useQuery} from '@tanstack/react-query'
|
||||
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate'
|
||||
import {Shapes_Stroke2_Corner0_Rounded as ShapesIcon} from '#/components/icons/Shapes'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import * as SettingsList from '../components/SettingsList'
|
||||
|
||||
Reference in New Issue
Block a user