import React, { useState, useCallback, useMemo } from "react"; import { View, Text, TouchableOpacity, Image, Platform, Pressable, } from "react-native"; import { FlashList } from "@shopify/flash-list"; import { Button } from "~/components/ui/button"; import { LucideChevronRight, LucideUser, LucidePlus, LucideSlidersHorizontal, } from "lucide-react-native"; import { ROUTES } from "~/lib/routes"; import { router } from "expo-router"; import ContactModal from "~/components/ui/contactModal"; import { Recipient } from "~/lib/services/recipientService"; import { Contact, useContactsStore, useRecipientsStore } from "~/lib/stores"; import TopBar from "~/components/ui/topBar"; import { Input } from "~/components/ui/input"; import ScreenWrapper from "~/components/ui/ScreenWrapper"; import ModalToast from "~/components/ui/toast"; import { useTranslation } from "react-i18next"; import Skeleton from "~/components/ui/skeleton"; // Contacts are only available on native platforms const isContactsSupported = Platform.OS !== "web"; // Helper function to create initials - moved outside component const getInitials = (name: string) => { return name .split(" ") .map((word) => word.charAt(0).toUpperCase()) .slice(0, 2) .join(""); }; // Individual Contact Card Component - Memoized for performance const ContactCard = React.memo( ({ contact, onPress }: { contact: Contact; onPress: () => void }) => { const { t } = useTranslation(); const primaryPhone = useMemo( () => contact.phoneNumbers?.find((phone) => phone.isPrimary) || contact.phoneNumbers?.[0], [contact.phoneNumbers] ); const initials = useMemo(() => getInitials(contact.name), [contact.name]); return ( {contact.imageAvailable && contact.image ? ( ) : ( {initials} )} {contact.name} {primaryPhone?.number || t("listrecipient.contactNoPhone")} ); }, (prevProps, nextProps) => { // Custom comparison function for better performance return ( prevProps.contact.id === nextProps.contact.id && prevProps.contact.name === nextProps.contact.name && prevProps.contact.imageAvailable === nextProps.contact.imageAvailable && prevProps.contact.image?.uri === nextProps.contact.image?.uri && prevProps.contact.phoneNumbers?.length === nextProps.contact.phoneNumbers?.length ); } ); // Individual Saved Recipient (Client) Card Component - Memoized for performance const SavedRecipientCard = React.memo( ({ recipient, onPress, clientType, accountsLabel, nextPaymentLabel, }: { recipient: Recipient; onPress: () => void; clientType: "Individual" | "Business"; accountsLabel: string; nextPaymentLabel?: string; }) => { const initials = useMemo( () => getInitials(recipient.fullName), [recipient.fullName] ); return ( {initials} {recipient.fullName} {clientType} {accountsLabel} {nextPaymentLabel && ( {nextPaymentLabel} )} ); }, (prevProps, nextProps) => { // Custom comparison function for better performance return ( prevProps.recipient.id === nextProps.recipient.id && prevProps.recipient.fullName === nextProps.recipient.fullName && prevProps.recipient.phoneNumber === nextProps.recipient.phoneNumber ); } ); export default function ListRecip() { const { contacts, loading, error, hasPermission, requestPermission } = useContactsStore(); const { recipients, loading: recipientsLoading, error: recipientsError, } = useRecipientsStore(); const { t } = useTranslation(); const [selectedContact, setSelectedContact] = useState(null); const [modalVisible, setModalVisible] = useState(false); const [searchQuery, setSearchQuery] = React.useState(""); const [toastVisible, setToastVisible] = React.useState(false); const [toastTitle, setToastTitle] = React.useState(""); const [toastDescription, setToastDescription] = React.useState< string | undefined >(undefined); const [toastVariant, setToastVariant] = React.useState< "success" | "error" | "warning" | "info" >("info"); const toastTimeoutRef = React.useRef | null>( null ); const showToast = ( title: string, description?: string, variant: "success" | "error" | "warning" | "info" = "info" ) => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } setToastTitle(title); setToastDescription(description); setToastVariant(variant); setToastVisible(true); toastTimeoutRef.current = setTimeout(() => { setToastVisible(false); toastTimeoutRef.current = null; }, 2500); }; React.useEffect(() => { return () => { if (toastTimeoutRef.current) { clearTimeout(toastTimeoutRef.current); } }; }, []); React.useEffect(() => { if (error) { showToast( t("listrecipient.toastErrorTitle"), t("listrecipient.toastContactsError"), "error" ); } else if (recipientsError) { showToast( t("listrecipient.toastErrorTitle"), t("listrecipient.toastRecipientsError"), "error" ); } }, [error, recipientsError, t]); const handleContactPress = useCallback((contact: Contact) => { setSelectedContact(contact); setModalVisible(true); }, []); const handleCloseModal = useCallback(() => { setModalVisible(false); setSelectedContact(null); }, []); const handleSendMoney = useCallback( (contact: Contact) => { // TODO: Navigate to send money screen with selected contact/recipient if (__DEV__) { console.log( "Send money to:", contact.name, "Phone:", contact.phoneNumbers?.[0]?.number ); } // Check if this is a saved recipient or device contact const isSavedRecipient = recipients.some( (recipient) => recipient.id === contact.id ); if (isSavedRecipient) { // router.push(`/sendorrequestmoney?recipientId=${contact.id}`); if (__DEV__) { console.log("This is a saved recipient"); } } else { // router.push(`/sendorrequestmoney?contactId=${contact.id}`); if (__DEV__) { console.log("This is a device contact"); } } }, [recipients] ); const handleRecipientPress = useCallback((recipient: Recipient) => { router.push({ pathname: ROUTES.RECIPIENT_DETAIL, params: { recipientId: recipient.id }, }); }, []); // Memoized renderItem function for FlashList performance const renderContactItem = useCallback( ({ item }: { item: Contact }) => ( handleContactPress(item)} /> ), [handleContactPress] ); // Memoized renderItem for saved recipients const renderRecipientItem = useCallback( ({ item, index }: { item: Recipient; index: number }) => { // Simple UI-only client typing and schedule data const lowerName = item.fullName.toLowerCase(); const isBusiness = lowerName.includes("ltd") || lowerName.includes("plc") || lowerName.includes("inc") || lowerName.includes("company"); const clientType: "Individual" | "Business" = isBusiness ? "Business" : "Individual"; const accountsCount = (index % 3) + 1; // 1–3 accounts, UI-only const accountsLabel = accountsCount === 1 ? "1 account" : `${accountsCount} accounts`; // Give every 2nd/3rd client a dummy schedule label let nextPaymentLabel: string | undefined; if (index % 3 === 1) { nextPaymentLabel = "Next payment: Every Monday · 10:00"; } else if (index % 3 === 2) { nextPaymentLabel = "Next payment: Monthly, 1st · 09:00"; } return ( handleRecipientPress(item)} clientType={clientType} accountsLabel={accountsLabel} nextPaymentLabel={nextPaymentLabel} /> ); }, [handleRecipientPress] ); // Memoized ItemSeparator component const ItemSeparator = useCallback(() => , []); // Memoized keyExtractor const keyExtractor = useCallback((item: Contact) => item.id, []); // Memoized keyExtractor for recipients const recipientKeyExtractor = useCallback((item: Recipient) => item.id, []); const normalizedSearch = searchQuery.trim().toLowerCase(); const filteredContacts = useMemo(() => { if (!normalizedSearch) return contacts; return contacts.filter((contact) => { const name = contact.name?.toLowerCase() ?? ""; const phones = (contact.phoneNumbers ?? []) .map((p) => p.number) .join(" ") .toLowerCase(); return ( name.includes(normalizedSearch) || phones.includes(normalizedSearch) ); }); }, [contacts, normalizedSearch]); const filteredRecipients = useMemo(() => { if (!normalizedSearch) return recipients; return recipients.filter((recipient) => { const name = recipient.fullName.toLowerCase(); const phone = recipient.phoneNumber.toLowerCase(); return ( name.includes(normalizedSearch) || phone.includes(normalizedSearch) ); }); }, [recipients, normalizedSearch]); const renderContent = () => { if (loading) { return ( {Array.from({ length: 8 }).map((_, index) => ( ))} ); } if (error) { return ( {t("listrecipient.contactsErrorTitle")} {error} ); } if (!hasPermission) { return ( {t("listrecipient.contactsPermissionTitle")} {t("listrecipient.contactsPermissionSubtitle")} ); } if (contacts.length === 0) { return ( {t("listrecipient.contactsEmptyTitle")} {t("listrecipient.contactsEmptySubtitle")} ); } return ( ); }; return ( {t("listrecipient.title")} } /> {/* Add Recipient Button */} {/* Saved Recipients List */} {t("listrecipient.savedRecipientsTitle", { count: recipients.length, })} {recipientsLoading ? ( {Array.from({ length: 4 }).map((_, index) => ( ))} ) : recipientsError ? ( {t("listrecipient.savedRecipientsError")} ) : recipients.length === 0 ? ( {t("listrecipient.savedRecipientsEmpty")} ) : ( {filteredRecipients.map((recipient, index) => { const lowerName = recipient.fullName.toLowerCase(); const isBusiness = lowerName.includes("ltd") || lowerName.includes("plc") || lowerName.includes("inc") || lowerName.includes("company"); const clientType: "Individual" | "Business" = isBusiness ? "Business" : "Individual"; const accountsCount = (index % 3) + 1; const accountsLabel = accountsCount === 1 ? "1 account" : `${accountsCount} accounts`; let nextPaymentLabel: string | undefined; if (index % 3 === 1) { nextPaymentLabel = "Next payment: Every Monday · 10:00"; } else if (index % 3 === 2) { nextPaymentLabel = "Next payment: Monthly, 1st · 09:00"; } return ( handleRecipientPress(recipient)} clientType={clientType} accountsLabel={accountsLabel} nextPaymentLabel={nextPaymentLabel} /> ); })} )} {/* Contacts list hidden for Agent app */} {/* Contact Modal */} ); }