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 */}
);
}