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