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.
100 lines
2.7 KiB
TypeScript
100 lines
2.7 KiB
TypeScript
import { apiContactToFullContact } from '@/lib/api/adapters'
|
|
import type { ApiContact } from '@/lib/api/types'
|
|
import type { FullContact } from '@/lib/contacts/types'
|
|
|
|
type BookListCache = {
|
|
apiContacts: ApiContact[]
|
|
fullContacts: FullContact[]
|
|
}
|
|
|
|
const parseCache = new Map<string, FullContact>()
|
|
const parseSigCache = new Map<string, string>()
|
|
const listCacheByBook = new Map<string, BookListCache>()
|
|
|
|
function contactCacheKey(api: ApiContact): string {
|
|
return api.uid || api.path || `${api.full_name}:${api.email ?? ''}`
|
|
}
|
|
|
|
function contactSignature(api: ApiContact): string {
|
|
return [
|
|
api.uid,
|
|
api.path,
|
|
api.etag,
|
|
api.full_name,
|
|
api.email,
|
|
api.raw_vcard?.length ?? 0,
|
|
].join('|')
|
|
}
|
|
|
|
/** Parse vCard une seule fois par contact tant que la signature API est stable. */
|
|
export function apiContactToFullContactCached(api: ApiContact): FullContact {
|
|
const key = contactCacheKey(api)
|
|
const sig = contactSignature(api)
|
|
if (parseSigCache.get(key) === sig) {
|
|
const hit = parseCache.get(key)
|
|
if (hit) return hit
|
|
}
|
|
const full = apiContactToFullContact(api)
|
|
parseCache.set(key, full)
|
|
parseSigCache.set(key, sig)
|
|
return full
|
|
}
|
|
|
|
function rebuildFullList(bookId: string, apiContacts: ApiContact[]): FullContact[] {
|
|
const fullContacts = apiContacts.map(apiContactToFullContactCached)
|
|
listCacheByBook.set(bookId, { apiContacts, fullContacts })
|
|
return fullContacts
|
|
}
|
|
|
|
/**
|
|
* Reconstruit la liste complète seulement si nécessaire.
|
|
* Sur ajout en fin de liste (cas courant), réutilise les contacts déjà parsés.
|
|
*/
|
|
export function mapApiContactsToFullContacts(
|
|
bookId: string,
|
|
apiContacts: ApiContact[] | undefined,
|
|
): FullContact[] {
|
|
if (!apiContacts) return []
|
|
|
|
const prev = listCacheByBook.get(bookId)
|
|
if (prev && prev.apiContacts === apiContacts) {
|
|
return prev.fullContacts
|
|
}
|
|
|
|
if (prev && apiContacts.length === prev.apiContacts.length + 1) {
|
|
let prefixMatch = true
|
|
for (let i = 0; i < prev.apiContacts.length; i++) {
|
|
if (apiContacts[i] !== prev.apiContacts[i]) {
|
|
prefixMatch = false
|
|
break
|
|
}
|
|
}
|
|
if (prefixMatch) {
|
|
const appended = apiContactToFullContactCached(apiContacts[apiContacts.length - 1]!)
|
|
const fullContacts = [...prev.fullContacts, appended]
|
|
listCacheByBook.set(bookId, { apiContacts, fullContacts })
|
|
return fullContacts
|
|
}
|
|
}
|
|
|
|
if (
|
|
prev &&
|
|
apiContacts.length === prev.apiContacts.length &&
|
|
apiContacts.every((c, i) => c === prev.apiContacts[i])
|
|
) {
|
|
return prev.fullContacts
|
|
}
|
|
|
|
return rebuildFullList(bookId, apiContacts)
|
|
}
|
|
|
|
export function invalidateContactListCache(bookId?: string) {
|
|
if (bookId) {
|
|
listCacheByBook.delete(bookId)
|
|
return
|
|
}
|
|
listCacheByBook.clear()
|
|
parseCache.clear()
|
|
parseSigCache.clear()
|
|
}
|