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