ultisuite-client/components/gmail/contacts-page/ignored-contacts-view.tsx
R3D347HR4Y 07d57f13a8
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
Add Contact Avatar Features and Improve UI Components
- Introduced new ContactAvatar and ContactAvatarPicker components for enhanced avatar management in contact views.
- Updated ContactDetailView and ContactFormView to utilize the new avatar components, improving user experience when adding or editing contacts.
- Enhanced ContactHoverCard and ContactRow components to display avatars, providing a more visually appealing interface.
- Added loading and error states in ContactsListView for better user feedback during data fetching.
- Implemented a new ContactsLoadState component to handle loading and error scenarios in the contacts list.
- Updated package.json to include @formkit/auto-animate for improved UI animations.
2026-06-06 20:26:51 +02:00

144 lines
4.8 KiB
TypeScript

"use client"
import { useCallback, useMemo, useState } from "react"
import { flushSync } from "react-dom"
import {
useAddDiscoveredContact,
useIgnoredDiscoveredContacts,
useRejectDiscoveredProfile,
} from "@/lib/api/hooks/use-contact-discovery"
import { fullContactToApiContact } from "@/lib/api/adapters"
import { profileDisplayName } from "@/lib/contacts/discovery-utils"
import type { ApiDiscoveredProfile, ApiDiscoveredProfileGroup } from "@/lib/contacts/discovery-types"
import type { FullContact } from "@/lib/contacts/types"
import { useContactsList } from "@/lib/contacts/use-contacts-list"
import {
CONTACTS_HEADING_TEXT,
CONTACTS_MUTED_TEXT,
CONTACTS_PAGE_SECTION_TITLE_CLASS,
} from "@/lib/contacts-chrome-classes"
import {
DiscoveryCardsMasonry,
DiscoveryCardsMasonryItem,
} from "@/components/gmail/contacts-page/discovery-cards-masonry"
import { SuggestedContactCard } from "@/components/gmail/contacts-page/suggested-contact-card"
import { cn } from "@/lib/utils"
interface IgnoredContactsViewProps {
searchQuery: string
}
function profileToGroup(profile: ApiDiscoveredProfile): ApiDiscoveredProfileGroup {
return {
group_key: profile.id,
profile_ids: [profile.id],
display_name: profileDisplayName(profile),
primary_email: profile.primary_email,
message_count: profile.message_count,
profile,
profiles: [profile],
}
}
export function IgnoredContactsView({ searchQuery }: IgnoredContactsViewProps) {
const { bookId } = useContactsList()
const { data: profiles = [], isLoading } = useIgnoredDiscoveredContacts()
const addDiscoveredContact = useAddDiscoveredContact()
const rejectProfile = useRejectDiscoveredProfile()
const [removedProfileIds, setRemovedProfileIds] = useState<Set<string>>(() => new Set())
const markProfileRemoved = useCallback((profileId: string) => {
setRemovedProfileIds((prev) => {
if (prev.has(profileId)) return prev
const next = new Set(prev)
next.add(profileId)
return next
})
}, [])
const restoreProfile = useCallback((profileId: string) => {
setRemovedProfileIds((prev) => {
if (!prev.has(profileId)) return prev
const next = new Set(prev)
next.delete(profileId)
return next
})
}, [])
const filtered = useMemo(() => {
const visible = profiles.filter((p) => !removedProfileIds.has(p.id))
const q = searchQuery.trim().toLowerCase()
if (!q) return visible
return visible.filter((p) => {
const name = profileDisplayName(p).toLowerCase()
return name.includes(q) || p.primary_email.toLowerCase().includes(q)
})
}, [profiles, searchQuery, removedProfileIds])
function handleAdd(profile: ApiDiscoveredProfile, buildContact: () => FullContact) {
if (!bookId) return
flushSync(() => markProfileRemoved(profile.id))
requestAnimationFrame(() => {
addDiscoveredContact.mutate(
{
bookId,
profileId: profile.id,
contact: fullContactToApiContact(buildContact()),
},
{ onError: () => restoreProfile(profile.id) },
)
})
}
function handleRemove(profileId: string) {
flushSync(() => markProfileRemoved(profileId))
requestAnimationFrame(() => {
rejectProfile.mutate(profileId, { onError: () => restoreProfile(profileId) })
})
}
const addingProfileId =
addDiscoveredContact.isPending && typeof addDiscoveredContact.variables?.profileId === "string"
? addDiscoveredContact.variables.profileId
: null
return (
<div className="px-6 py-6 text-foreground">
<h2 className={cn("mb-2 text-base font-medium", CONTACTS_HEADING_TEXT)}>Ignorés</h2>
<p className={cn("mb-6 text-sm", CONTACTS_MUTED_TEXT)}>
Ces expéditeurs ne sont pas dans vos contacts et ne seront plus suggérés. Vous pouvez les
ajouter à votre carnet ou les supprimer définitivement.
</p>
<h3 className={cn("mb-4", CONTACTS_PAGE_SECTION_TITLE_CLASS)}>
{filtered.length} contact{filtered.length !== 1 ? "s" : ""}
</h3>
{isLoading && (
<p className={cn("py-8 text-center text-sm", CONTACTS_MUTED_TEXT)}>Chargement</p>
)}
{!isLoading && filtered.length === 0 && (
<p className={cn("py-8 text-center text-sm", CONTACTS_MUTED_TEXT)}>
Aucun contact ignoré.
</p>
)}
<DiscoveryCardsMasonry>
{filtered.map((p) => (
<DiscoveryCardsMasonryItem key={p.id}>
<SuggestedContactCard
group={profileToGroup(p)}
mode="ignored"
addBusy={addingProfileId === p.id}
busy={rejectProfile.isPending && rejectProfile.variables === p.id}
onAdd={(buildContact) => handleAdd(p, buildContact)}
onRemove={() => handleRemove(p.id)}
/>
</DiscoveryCardsMasonryItem>
))}
</DiscoveryCardsMasonry>
</div>
)
}