1import { useFocusEffect } from '@react-navigation/native'; 2import { StackNavigationProp } from '@react-navigation/stack'; 3import { Platform } from '@unimodules/core'; 4import * as Contacts from 'expo-contacts'; 5import React from 'react'; 6import { RefreshControl, StyleSheet, Text, View } from 'react-native'; 7 8import HeaderIconButton, { HeaderContainerRight } from '../../components/HeaderIconButton'; 9import usePermissions from '../../utilities/usePermissions'; 10import { useResolvedValue } from '../../utilities/useResolvedValue'; 11import * as ContactUtils from './ContactUtils'; 12import ContactsList from './ContactsList'; 13 14type StackParams = { 15 ContactDetail: { id: string }; 16}; 17 18type Props = { 19 navigation: StackNavigationProp<StackParams>; 20}; 21 22const CONTACT_PAGE_SIZE = 500; 23 24export default function ContactsScreen({ navigation }: Props) { 25 const [isAvailable, error] = useResolvedValue(Contacts.isAvailableAsync); 26 const [permission] = usePermissions(Contacts.requestPermissionsAsync); 27 28 const warning = React.useMemo(() => { 29 if (error) { 30 return `An unknown error occurred while checking the API availability: ${error.message}`; 31 } else if (isAvailable === null) { 32 return 'Checking availability...'; 33 } else if (isAvailable === false) { 34 return 'Contacts API is not available on this platform.'; 35 } else if (!permission) { 36 return 'Contacts permission has not been granted for this app. Grant permission in the Settings app to continue.'; 37 } else if (permission) { 38 return null; 39 } 40 return 'Pending user permission...'; 41 }, [error, permission, isAvailable]); 42 43 if (warning) { 44 return ( 45 <View style={styles.permissionContainer}> 46 <Text>{warning}</Text> 47 </View> 48 ); 49 } 50 51 return <ContactsView navigation={navigation} />; 52} 53 54function ContactsView({ navigation }: Props) { 55 let rawContacts: Record<string, Contacts.Contact> = {}; 56 57 const [contacts, setContacts] = React.useState<Contacts.Contact[]>([]); 58 const [hasNextPage, setHasNextPage] = React.useState(true); 59 const [refreshing, setRefreshing] = React.useState(false); 60 61 const onPressItem = React.useCallback( 62 (id: string) => { 63 navigation.navigate('ContactDetail', { id }); 64 }, 65 [navigation] 66 ); 67 68 const loadAsync = async (event: { distanceFromEnd?: number } = {}, restart = false) => { 69 if (!hasNextPage || refreshing || Platform.OS === 'web') { 70 return; 71 } 72 setRefreshing(true); 73 74 const pageOffset = restart ? 0 : contacts.length || 0; 75 76 const pageSize = restart ? Math.max(pageOffset, CONTACT_PAGE_SIZE) : CONTACT_PAGE_SIZE; 77 78 const payload = await Contacts.getContactsAsync({ 79 fields: [Contacts.Fields.Name], 80 sort: Contacts.SortTypes.LastName, 81 pageSize, 82 pageOffset, 83 }); 84 85 const { data: nextContacts } = payload; 86 87 if (restart) { 88 rawContacts = {}; 89 } 90 91 for (const contact of nextContacts) { 92 rawContacts[contact.id] = contact; 93 } 94 setContacts(Object.values(rawContacts)); 95 setHasNextPage(payload.hasNextPage); 96 setRefreshing(false); 97 }; 98 99 const onFocus = React.useCallback(() => { 100 loadAsync(); 101 }, []); 102 103 useFocusEffect(onFocus); 104 105 return ( 106 <ContactsList 107 onEndReachedThreshold={-1.5} 108 refreshControl={ 109 <RefreshControl refreshing={refreshing} onRefresh={() => loadAsync({}, true)} /> 110 } 111 data={contacts} 112 onPressItem={onPressItem} 113 onEndReached={loadAsync} 114 /> 115 ); 116} 117 118const styles = StyleSheet.create({ 119 button: { 120 marginVertical: 10, 121 }, 122 permissionContainer: { 123 flex: 1, 124 justifyContent: 'center', 125 alignItems: 'center', 126 }, 127 contactRow: { 128 marginBottom: 12, 129 }, 130}); 131 132ContactsScreen.navigationOptions = () => { 133 return { 134 title: 'Contacts', 135 headerRight: () => ( 136 <HeaderContainerRight> 137 <HeaderIconButton 138 disabled={Platform.select({ web: true, default: false })} 139 name="md-add" 140 onPress={() => { 141 const randomContact = { note: 'Likes expo...' } as Contacts.Contact; 142 ContactUtils.presentNewContactFormAsync({ contact: randomContact }); 143 }} 144 /> 145 </HeaderContainerRight> 146 ), 147 }; 148}; 149