Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- 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.
144 lines
4.8 KiB
TypeScript
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>
|
|
)
|
|
}
|