add empty state screen
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 64 64"><path fill="#000" d="M24.952 33.435c10.897 0 19.195 6.496 22.58 15.65.784 2.117.258 4.183-1 5.683-1.242 1.48-3.194 2.414-5.33 2.415h-32.5c-2.136 0-4.089-.935-5.331-2.415-1.258-1.5-1.783-3.566-1-5.682 3.386-9.155 11.683-15.651 22.58-15.651Zm0 2.298c-9.885 0-17.355 5.849-20.425 14.15-.47 1.271-.174 2.48.604 3.407.795.947 2.096 1.594 3.57 1.594h32.5c1.476 0 2.777-.647 3.571-1.594.778-.928 1.074-2.136.605-3.406-3.07-8.302-10.54-14.15-20.425-14.15Zm33.262-14.59a1.15 1.15 0 1 1 1.625 1.626l-5.688 5.687 5.686 5.688a1.15 1.15 0 1 1-1.625 1.625l-5.686-5.688-5.687 5.688a1.15 1.15 0 1 1-1.625-1.625l5.687-5.688-5.689-5.687a1.15 1.15 0 1 1 1.625-1.625l5.689 5.687 5.688-5.687ZM24.949 5.552c6.557 0 11.874 5.316 11.874 11.874 0 6.557-5.317 11.874-11.874 11.874s-11.874-5.317-11.874-11.874S18.39 5.55 24.949 5.55Zm0 2.299a9.575 9.575 0 0 0-9.575 9.575A9.575 9.575 0 0 0 24.949 27a9.575 9.575 0 0 0 9.575-9.575A9.575 9.575 0 0 0 24.95 7.85Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1014 B |
@@ -1,10 +1,11 @@
|
||||
import {useMemo, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import * as Contacts from 'expo-contacts'
|
||||
import * as SMS from 'expo-sms'
|
||||
import {type ModerationOpts} from '@atproto/api'
|
||||
import {msg, Plural, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation, useQueryClient} from '@tanstack/react-query'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {cleanError, isNetworkError} from '#/lib/strings/errors'
|
||||
@@ -26,6 +27,7 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {Envelope_Stroke2_Corner0_Rounded as EnvelopeIcon} from '#/components/icons/Envelope'
|
||||
import {MagnifyingGlassX_Stroke2_Corner0_Rounded_Large as SearchFailedIcon} from '#/components/icons/MagnifyingGlass'
|
||||
import {PersonX_Stroke2_Corner0_Rounded_Large as PersonXIcon} from '#/components/icons/Person'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||
import * as Layout from '#/components/Layout'
|
||||
@@ -60,17 +62,16 @@ type Item =
|
||||
type: 'search empty state'
|
||||
query: string
|
||||
}
|
||||
// we could use Contacts.presentAccessPickerAsync() if we get limited permissions?
|
||||
// | {
|
||||
// type: 'request more contacts'
|
||||
// }
|
||||
| {
|
||||
type: 'totally empty state'
|
||||
}
|
||||
|
||||
export function ViewMatches({
|
||||
state,
|
||||
dispatch,
|
||||
}: {
|
||||
state: Extract<State, {step: '4: view matches'}>
|
||||
dispatch: React.Dispatch<Action>
|
||||
dispatch: React.ActionDispatch<[Action]>
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const gutter = useGutters([0, 'wide'])
|
||||
@@ -84,6 +85,7 @@ export function ViewMatches({
|
||||
handles: ['pfrazee.com', 'internet.bsky.social', 'darrin.bsky.team'],
|
||||
})
|
||||
state.matches = profiles?.profiles?.map(profile => ({profile})) ?? []
|
||||
// state.contacts = []
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const {
|
||||
@@ -180,14 +182,21 @@ export function ViewMatches({
|
||||
const profile = match.profile
|
||||
all.push({type: 'match', profile})
|
||||
}
|
||||
all.push({type: 'contacts header'})
|
||||
} else {
|
||||
|
||||
if (state.contacts.length > 0) {
|
||||
all.push({type: 'contacts header'})
|
||||
}
|
||||
} else if (state.contacts.length > 0) {
|
||||
all.push({type: 'no matches header'})
|
||||
}
|
||||
|
||||
for (const contact of state.contacts) {
|
||||
all.push({type: 'contact', contact})
|
||||
}
|
||||
|
||||
if (all.length === 0) {
|
||||
all.push({type: 'totally empty state'})
|
||||
}
|
||||
}
|
||||
|
||||
return all
|
||||
@@ -276,6 +285,7 @@ export function ViewMatches({
|
||||
return (
|
||||
<Header
|
||||
titleText={_(msg`You got here first`)}
|
||||
largeTitle
|
||||
subtitleText={_(
|
||||
msg`Bluesky is more fun with friends. Do you want to invite some of yours?`,
|
||||
)}
|
||||
@@ -283,10 +293,15 @@ export function ViewMatches({
|
||||
)
|
||||
case 'search empty state':
|
||||
return <SearchEmptyState query={item.query} />
|
||||
case 'totally empty state':
|
||||
return <TotallyEmptyState dispatch={dispatch} />
|
||||
}
|
||||
}
|
||||
|
||||
const isEmpty = items?.[0]?.type === 'search empty state'
|
||||
const isSearchEmpty = items?.[0]?.type === 'search empty state'
|
||||
const isTotallyEmpty = items?.[0]?.type === 'totally empty state'
|
||||
|
||||
const isEmpty = isSearchEmpty || isTotallyEmpty
|
||||
|
||||
return (
|
||||
<View style={[a.h_full]}>
|
||||
@@ -295,25 +310,27 @@ export function ViewMatches({
|
||||
<Layout.Header.Content />
|
||||
<Layout.Header.Slot />
|
||||
</Layout.Header.Outer>
|
||||
<View style={[gutter, a.mb_md]}>
|
||||
<SearchInput
|
||||
placeholder={_(msg`Search contacts`)}
|
||||
value={search}
|
||||
onFocus={() => {
|
||||
onFocus()
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
onBlur={() => {
|
||||
onBlur()
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
onChangeText={text => {
|
||||
setSearch(text)
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
onClearText={() => setSearch('')}
|
||||
/>
|
||||
</View>
|
||||
{!isTotallyEmpty && (
|
||||
<View style={[gutter, a.mb_md]}>
|
||||
<SearchInput
|
||||
placeholder={_(msg`Search contacts`)}
|
||||
value={search}
|
||||
onFocus={() => {
|
||||
onFocus()
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
onBlur={() => {
|
||||
onBlur()
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
onChangeText={text => {
|
||||
setSearch(text)
|
||||
listRef.current?.scrollToOffset({offset: 0, animated: false})
|
||||
}}
|
||||
onClearText={() => setSearch('')}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
<List
|
||||
ref={listRef}
|
||||
data={items}
|
||||
@@ -458,11 +475,13 @@ function ContactItem({contact}: {contact: Contact}) {
|
||||
|
||||
function Header({
|
||||
titleText,
|
||||
largeTitle,
|
||||
subtitleText,
|
||||
children,
|
||||
hasContentAbove,
|
||||
}: {
|
||||
titleText: React.ReactNode
|
||||
largeTitle?: boolean
|
||||
subtitleText?: React.ReactNode
|
||||
children?: React.ReactNode
|
||||
hasContentAbove?: boolean
|
||||
@@ -481,7 +500,9 @@ function Header({
|
||||
: a.pt_md,
|
||||
]}>
|
||||
<View style={[a.flex_row, a.align_center, a.justify_between]}>
|
||||
<Text style={[a.text_3xl, a.font_bold]}>{titleText}</Text>
|
||||
<Text style={[largeTitle ? a.text_3xl : a.text_xl, a.font_bold]}>
|
||||
{titleText}
|
||||
</Text>
|
||||
{children}
|
||||
</View>
|
||||
{subtitleText && (
|
||||
@@ -520,3 +541,112 @@ function SearchEmptyState({query}: {query: string}) {
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function TotallyEmptyState({
|
||||
dispatch,
|
||||
}: {
|
||||
dispatch: React.ActionDispatch<[Action]>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const {data} = useQuery({
|
||||
queryKey: ['contacts-permissions'],
|
||||
queryFn: async () => await Contacts.getPermissionsAsync(),
|
||||
})
|
||||
|
||||
const {mutate: uploadContacts, isPending: isUploadPending} = useMutation({
|
||||
mutationFn: async (_contacts: Contacts.ExistingContact[]) => {
|
||||
await wait(2e3, () => {})
|
||||
},
|
||||
onSuccess: () => {
|
||||
dispatch({
|
||||
type: 'SYNC_CONTACTS_SUCCESS',
|
||||
payload: {
|
||||
matches: [],
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const {mutate: addMore, isPending: isAddMorePending} = useMutation({
|
||||
mutationFn: async () => {
|
||||
await Contacts.presentAccessPickerAsync()
|
||||
|
||||
const contacts = await Contacts.getContactsAsync({
|
||||
fields: [
|
||||
Contacts.Fields.FirstName,
|
||||
Contacts.Fields.LastName,
|
||||
Contacts.Fields.PhoneNumbers,
|
||||
Contacts.Fields.Image,
|
||||
],
|
||||
})
|
||||
|
||||
return contacts.data
|
||||
},
|
||||
onSuccess: contacts => {
|
||||
dispatch({
|
||||
type: 'GET_CONTACTS_SUCCESS',
|
||||
payload: {contacts},
|
||||
})
|
||||
uploadContacts(contacts)
|
||||
},
|
||||
onError: err => {
|
||||
logger.error('Add more contacts failed', {safeMessage: err})
|
||||
Toast.show(_(msg`Could not add more contacts. ${cleanError(err)}`))
|
||||
},
|
||||
})
|
||||
|
||||
const isPending = isAddMorePending || isUploadPending
|
||||
|
||||
const isLimited = data?.accessPrivileges === 'limited'
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.flex_col,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.gap_xs,
|
||||
{paddingTop: 140},
|
||||
a.px_5xl,
|
||||
]}>
|
||||
<PersonXIcon width={64} style={[t.atoms.text_contrast_low]} />
|
||||
<Text
|
||||
style={[
|
||||
a.text_xl,
|
||||
a.font_bold,
|
||||
a.leading_snug,
|
||||
a.text_center,
|
||||
a.mt_md,
|
||||
]}>
|
||||
<Trans>No contacts found</Trans>
|
||||
</Text>
|
||||
{isLimited && (
|
||||
<>
|
||||
<Text
|
||||
style={[
|
||||
a.text_md,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.leading_snug,
|
||||
a.text_center,
|
||||
]}>
|
||||
<Trans>Would you like to try again?</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={_(msg`Try adding more contacts`)}
|
||||
onPress={() => addMore()}
|
||||
disabled={isPending}
|
||||
color="secondary"
|
||||
size="small"
|
||||
style={[a.mt_md]}>
|
||||
<ButtonText>
|
||||
<Trans>Try adding more contacts</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z',
|
||||
})
|
||||
|
||||
export const PersonX_Stroke2_Corner0_Rounded_Large = createSinglePathSVG({
|
||||
viewBox: '0 0 64 64',
|
||||
path: 'M24.952 33.435c10.897 0 19.195 6.496 22.58 15.65.784 2.117.258 4.183-1 5.683-1.242 1.48-3.194 2.414-5.33 2.415h-32.5c-2.136 0-4.089-.935-5.331-2.415-1.258-1.5-1.783-3.566-1-5.682 3.386-9.155 11.683-15.651 22.58-15.651Zm0 2.298c-9.885 0-17.355 5.849-20.425 14.15-.47 1.271-.174 2.48.604 3.407.795.947 2.096 1.594 3.57 1.594h32.5c1.476 0 2.777-.647 3.571-1.594.778-.928 1.074-2.136.605-3.406-3.07-8.302-10.54-14.15-20.425-14.15Zm33.262-14.59a1.15 1.15 0 1 1 1.625 1.626l-5.688 5.687 5.686 5.688a1.15 1.15 0 1 1-1.625 1.625l-5.686-5.688-5.687 5.688a1.15 1.15 0 1 1-1.625-1.625l5.687-5.688-5.689-5.687a1.15 1.15 0 1 1 1.625-1.625l5.689 5.687 5.688-5.687ZM24.949 5.552c6.557 0 11.874 5.316 11.874 11.874 0 6.557-5.317 11.874-11.874 11.874s-11.874-5.317-11.874-11.874S18.39 5.55 24.949 5.55Zm0 2.299a9.575 9.575 0 0 0-9.575 9.575A9.575 9.575 0 0 0 24.949 27a9.575 9.575 0 0 0 9.575-9.575A9.575 9.575 0 0 0 24.95 7.85Z',
|
||||
})
|
||||
|
||||
export const PersonPlus_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.678 19c.71-2.909 3.092-5 6.322-5 .621 0 1.206.077 1.748.218a1 1 0 1 0 .504-1.936A8.931 8.931 0 0 0 12 12c-4.758 0-8.083 3.521-8.496 7.906A1 1 0 0 0 4.5 21H11a1 1 0 1 0 0-2H5.678ZM18 14a1 1 0 0 1 1 1v2h2a1 1 0 1 1 0 2h-2v2a1 1 0 1 1-2 0v-2h-2a1 1 0 1 1 0-2h2v-2a1 1 0 0 1 1-1Z',
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user