Files
weflow-new/src/pages/Export/utils/avatar.ts
T
2026-06-30 00:18:31 +08:00

161 lines
5.7 KiB
TypeScript

/**
* ExportV2 — Avatar utility functions
* Pure functions for avatar URL normalization, caching logic, and merge operations.
*/
import type { ContactInfo } from '../../../types/models'
import type * as configService from '../../../services/config'
import { INLINE_AVATAR_CACHE_MAX_LENGTH } from '../constants'
// ─── URL normalization ───────────────────────────────────────
export const normalizeExportAvatarUrl = (value?: string | null): string | undefined => {
const normalized = String(value || '').trim()
if (!normalized) return undefined
const lower = normalized.toLowerCase()
if (lower === 'null' || lower === 'undefined') return undefined
return normalized
}
export const shouldPersistExportAvatarUrl = (value?: string | null): value is string => {
const normalized = normalizeExportAvatarUrl(value)
if (!normalized) return false
if (!normalized.startsWith('data:')) return true
return normalized.length <= INLINE_AVATAR_CACHE_MAX_LENGTH
}
// ─── Contact map from caches ─────────────────────────────────
export const toContactMapFromCaches = (
contacts: configService.ContactsListCacheContact[],
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>
): Record<string, ContactInfo> => {
const map: Record<string, ContactInfo> = {}
for (const contact of contacts || []) {
if (!contact?.username) continue
const cachedAvatarUrl = avatarEntries[contact.username]?.avatarUrl
map[contact.username] = {
...contact,
avatarUrl: shouldPersistExportAvatarUrl(cachedAvatarUrl) ? cachedAvatarUrl : undefined
}
}
return map
}
// ─── Avatar cache compaction ─────────────────────────────────
export const compactExportAvatarEntries = (
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>
): {
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>
changed: boolean
} => {
const nextCache: Record<string, configService.ContactsAvatarCacheEntry> = {}
let changed = false
for (const [username, entry] of Object.entries(avatarEntries || {})) {
const normalizedUsername = String(username || '').trim()
const avatarUrl = normalizeExportAvatarUrl(entry?.avatarUrl)
if (!normalizedUsername || !shouldPersistExportAvatarUrl(avatarUrl)) {
changed = true
continue
}
nextCache[normalizedUsername] = {
avatarUrl,
updatedAt: Number(entry?.updatedAt || 0) || Date.now(),
checkedAt: Number(entry?.checkedAt || 0) || Date.now()
}
if (
normalizedUsername !== username ||
avatarUrl !== entry?.avatarUrl ||
nextCache[normalizedUsername].updatedAt !== entry?.updatedAt ||
nextCache[normalizedUsername].checkedAt !== entry?.checkedAt
) {
changed = true
}
}
return { avatarEntries: nextCache, changed }
}
// ─── Merge avatar cache into contacts ────────────────────────
export const mergeAvatarCacheIntoContacts = (
sourceContacts: ContactInfo[],
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>
): ContactInfo[] => {
if (!sourceContacts.length || Object.keys(avatarEntries).length === 0) {
return sourceContacts
}
let changed = false
const merged = sourceContacts.map((contact) => {
const cachedAvatar = avatarEntries[contact.username]?.avatarUrl
if (!shouldPersistExportAvatarUrl(cachedAvatar) || contact.avatarUrl) {
return contact
}
changed = true
return {
...contact,
avatarUrl: cachedAvatar
}
})
return changed ? merged : sourceContacts
}
// ─── Upsert avatar cache from contacts ───────────────────────
export const upsertAvatarCacheFromContacts = (
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>,
sourceContacts: ContactInfo[],
options?: { prune?: boolean; markCheckedUsernames?: string[]; now?: number }
): {
avatarEntries: Record<string, configService.ContactsAvatarCacheEntry>
changed: boolean
updatedAt: number | null
} => {
const compactedCache = compactExportAvatarEntries(avatarEntries)
const nextCache = { ...compactedCache.avatarEntries }
const now = options?.now || Date.now()
const markCheckedSet = new Set((options?.markCheckedUsernames || []).filter(Boolean))
const usernamesInSource = new Set<string>()
let changed = compactedCache.changed
for (const contact of sourceContacts) {
const username = String(contact.username || '').trim()
if (!username) continue
usernamesInSource.add(username)
const prev = nextCache[username]
const avatarUrl = normalizeExportAvatarUrl(contact.avatarUrl)
if (!shouldPersistExportAvatarUrl(avatarUrl)) continue
const updatedAt = !prev || prev.avatarUrl !== avatarUrl ? now : prev.updatedAt
const checkedAt = markCheckedSet.has(username) ? now : (prev?.checkedAt || now)
if (!prev || prev.avatarUrl !== avatarUrl || prev.updatedAt !== updatedAt || prev.checkedAt !== checkedAt) {
nextCache[username] = { avatarUrl, updatedAt, checkedAt }
changed = true
}
}
for (const username of markCheckedSet) {
const prev = nextCache[username]
if (!prev) continue
if (prev.checkedAt !== now) {
nextCache[username] = { ...prev, checkedAt: now }
changed = true
}
}
if (options?.prune) {
for (const username of Object.keys(nextCache)) {
if (usernamesInSource.has(username)) continue
delete nextCache[username]
changed = true
}
}
return {
avatarEntries: nextCache,
changed,
updatedAt: changed ? now : null
}
}