"use client" import { useEffect, useMemo, useRef, useCallback, useState, useDeferredValue } from "react" import { Loader2, RefreshCw, X } from "lucide-react" import { Button } from "@/components/ui/button" import { Progress } from "@/components/ui/progress" import { useActiveDiscoveryScan, useAddDiscoveredContact, useBlockDiscoveredProfile, useCancelDiscoveryScan, useDiscoveryCounts, useEnrichDiscoveredProfile, useIgnoreDiscoveredProfile, extractDiscoveryGroupEmails, flattenOtherContactPages, useOtherDiscoveredContacts, useStartDiscoveryScan, scanProgressLabel, } from "@/lib/api/hooks/use-contact-discovery" import { fullContactToApiContact } from "@/lib/api/adapters" import { useContactsList } from "@/lib/contacts/use-contacts-list" import { isSuggestableDiscoveryGroup, } from "@/lib/contacts/discovery-utils" import { discoveryGroupReactKey, normalizeDiscoveryGroup, } from "@/lib/contacts/discovery-grouping" import type { ApiDiscoveredProfileGroup } from "@/lib/contacts/discovery-types" import type { FullContact } from "@/lib/contacts/types" import { useBlockedSendersStore } from "@/lib/stores/blocked-senders-store" import { CONTACTS_HEADING_TEXT, CONTACTS_MUTED_TEXT, CONTACTS_PAGE_INFO_BANNER_CLASS, CONTACTS_PAGE_INFO_BANNER_ICON_CLASS, CONTACTS_PAGE_SECTION_TITLE_CLASS, CONTACTS_PRIMARY_BTN_CLASS, } from "@/lib/contacts-chrome-classes" import { DiscoveryCardsMasonry, DiscoveryCardsMasonryItem, DiscoveryCardsMasonrySentinel, } from "@/components/gmail/contacts-page/discovery-cards-masonry" import { SuggestedContactCard } from "@/components/gmail/contacts-page/suggested-contact-card" import { useDiscoveryScrollLoad } from "@/components/gmail/contacts-page/use-discovery-scroll-load" import { cn } from "@/lib/utils" interface OtherContactsViewProps { searchQuery: string } export function OtherContactsView({ searchQuery }: OtherContactsViewProps) { const deferredSearchQuery = useDeferredValue(searchQuery.trim()) const { bookId } = useContactsList() const { data: discoveryCounts } = useDiscoveryCounts() const { data: activeScan } = useActiveDiscoveryScan() const isRunning = activeScan != null && (activeScan.status === "running" || activeScan.status === "pending") const { data, isLoading, refetch, fetchNextPage, hasNextPage, isFetchingNextPage, } = useOtherDiscoveredContacts(!isRunning, deferredSearchQuery) const groups = useMemo(() => flattenOtherContactPages(data), [data]) const loadMoreRef = useRef(null) const startScan = useStartDiscoveryScan() const cancelScan = useCancelDiscoveryScan() const addDiscoveredContact = useAddDiscoveredContact() const ignoreProfile = useIgnoreDiscoveredProfile() const blockProfile = useBlockDiscoveredProfile() const enrichProfile = useEnrichDiscoveredProfile() const blockSenders = useBlockedSendersStore((s) => s.blockSenders) const completedHandled = useRef(false) const [dismissedKeys, setDismissedKeys] = useState>(() => new Set()) const dismissCard = useCallback((cardKey: string) => { setDismissedKeys((prev) => { if (prev.has(cardKey)) return prev const next = new Set(prev) next.add(cardKey) return next }) }, []) const restoreCard = useCallback((cardKey: string) => { setDismissedKeys((prev) => { if (!prev.has(cardKey)) return prev const next = new Set(prev) next.delete(cardKey) return next }) }, []) useEffect(() => { if (completedHandled.current && !isRunning) return if (!isRunning && activeScan == null) { completedHandled.current = true void refetch() } }, [isRunning, activeScan, refetch]) useEffect(() => { if (isRunning) completedHandled.current = false }, [isRunning]) const filtered = useMemo(() => { return groups .map((g) => normalizeDiscoveryGroup(g)) .filter((g): g is ApiDiscoveredProfileGroup => g != null) .filter(isSuggestableDiscoveryGroup) .filter((g) => !dismissedKeys.has(discoveryGroupReactKey(g))) }, [groups, dismissedKeys]) const isSearchPending = searchQuery.trim() !== deferredSearchQuery const serverSuggestionCount = deferredSearchQuery ? (data?.pages[0]?.total ?? filtered.length) : (discoveryCounts?.other_contacts ?? data?.pages[0]?.total ?? filtered.length) const suggestionCountLabel = deferredSearchQuery ? String(filtered.length) : hasNextPage ? `${filtered.length} / ${serverSuggestionCount}` : String(filtered.length) const loadMore = useCallback(() => { if (!hasNextPage || isFetchingNextPage) return void fetchNextPage() }, [hasNextPage, isFetchingNextPage, fetchNextPage]) useDiscoveryScrollLoad({ sentinelRef: loadMoreRef, hasNextPage: Boolean(hasNextPage), isFetchingNextPage, onLoadMore: loadMore, maxAutoLoads: 1, }) function handleScan() { if (isRunning) return startScan.mutate(bookId) } function handleAdd(group: ApiDiscoveredProfileGroup, buildContact: () => FullContact) { const profileId = group.profile?.id ?? group.profile_ids[0] if (!profileId || !bookId) return const cardKey = discoveryGroupReactKey(group) dismissCard(cardKey) addDiscoveredContact.mutate( { bookId, profileId, contact: fullContactToApiContact(buildContact()), }, { onError: () => restoreCard(cardKey) }, ) } function handleIgnore(group: ApiDiscoveredProfileGroup, profileId: string) { const cardKey = discoveryGroupReactKey(group) dismissCard(cardKey) ignoreProfile.mutate(profileId, { onError: () => restoreCard(cardKey) }) } function handleBlock(group: ApiDiscoveredProfileGroup, profileId: string) { const cardKey = discoveryGroupReactKey(group) const emails = extractDiscoveryGroupEmails(group) if (emails.length) blockSenders(emails) dismissCard(cardKey) blockProfile.mutate(profileId, { onSuccess: (res) => { if (res.emails?.length) blockSenders(res.emails) }, onError: () => restoreCard(cardKey), }) } const enrichingProfileId = enrichProfile.isPending && typeof enrichProfile.variables === "string" ? enrichProfile.variables : null const progress = activeScan?.progress_percent ?? 0 const scanLabel = activeScan ? scanProgressLabel(activeScan) : null return (
📬

Contacts détectés dans vos e-mails

{isRunning && ( )}

Analyse complète de toutes vos boîtes mail. Les listes de diffusion, e-mails jetables et expéditeurs spam sont exclus.

{activeScan?.status === "failed" && activeScan.error_message && (

{activeScan.error_message}

)} {isRunning && scanLabel && activeScan && (

{scanLabel}

)}

Suggestions ({suggestionCountLabel})

{isLoading && !isRunning && (

{isSearchPending ? "Recherche…" : "Chargement…"}

)} {!isLoading && filtered.length === 0 && !isRunning && (

{deferredSearchQuery ? "Aucun contact ne correspond à votre recherche." : "Aucun contact détecté. Lancez une analyse pour scanner tous vos messages."}

)} {isFetchingNextPage ? ( <>

Chargement…

) : (

{deferredSearchQuery ? `${filtered.length} sur ${serverSuggestionCount} résultats` : `${filtered.length} sur ${serverSuggestionCount} affichés`}

)} ) : null } > {filtered.map((group) => { const profileId = group.profile?.id ?? group.profile_ids[0] if (!profileId) return null const reactKey = discoveryGroupReactKey(group) return ( handleAdd(group, buildContact)} onEnrich={() => enrichProfile.mutate(profileId)} onIgnore={() => handleIgnore(group, profileId)} onBlock={() => handleBlock(group, profileId)} /> ) })}
) }