diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index eb5cfe4..4a71e88 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -5055,7 +5055,17 @@ class ChatService { const contact = await this.getContact(username) const avatarResult = await wcdbService.getAvatarUrls([username]) - const avatarUrl = avatarResult.success && avatarResult.map ? avatarResult.map[username] : undefined + let avatarUrl = avatarResult.success && avatarResult.map ? avatarResult.map[username] : undefined + if (!this.isValidAvatarUrl(avatarUrl)) { + avatarUrl = undefined + } + if (!avatarUrl) { + const headImageAvatars = await this.getAvatarsFromHeadImageDb([username]) + const fallbackAvatarUrl = headImageAvatars[username] + if (this.isValidAvatarUrl(fallbackAvatarUrl)) { + avatarUrl = fallbackAvatarUrl + } + } const displayName = contact?.remark || contact?.nickName || contact?.alias || cached?.displayName || username const cacheEntry: ContactCacheEntry = { avatarUrl, @@ -5523,6 +5533,13 @@ class ChatService { avatarUrl = avatarCandidate } } + if (!avatarUrl) { + const headImageAvatars = await this.getAvatarsFromHeadImageDb([normalizedSessionId]) + const fallbackAvatarUrl = headImageAvatars[normalizedSessionId] + if (this.isValidAvatarUrl(fallbackAvatarUrl)) { + avatarUrl = fallbackAvatarUrl + } + } if (!Number.isFinite(messageCount)) { messageCount = messageCountResult.status === 'fulfilled' && diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 549b4ea..1f4a6cc 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -49,6 +49,7 @@ import { SnsPostItem } from '../components/Sns/SnsPostItem' import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog' import { ExportDateRangeDialog } from '../components/Export/ExportDateRangeDialog' import { ExportDefaultsSettingsForm, type ExportDefaultsSettingsPatch } from '../components/Export/ExportDefaultsSettingsForm' +import { Avatar } from '../components/Avatar' import type { SnsPost } from '../types/sns' import { cloneExportDateRange, @@ -538,6 +539,14 @@ const getAvatarLetter = (name: string): string => { return [...name][0] || '?' } +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 +} + const toComparableNameSet = (values: Array): Set => { const set = new Set() for (const value of values) { @@ -1713,6 +1722,7 @@ function ExportPage() { startIndex: 0, endIndex: -1 }) + const avatarHydrationRequestedRef = useRef>(new Set()) const sessionMutualFriendsMetricsRef = useRef>({}) const sessionMutualFriendsDirectMetricsRef = useRef>({}) const sessionMutualFriendsQueueRef = useRef([]) @@ -1957,6 +1967,7 @@ function ExportPage() { displayName: contact.displayName, remark: contact.remark, nickname: contact.nickname, + alias: contact.alias, type: contact.type })) ).catch((error) => { @@ -1998,6 +2009,94 @@ function ExportPage() { } }, [ensureExportCacheScope, syncContactTypeCounts]) + const hydrateVisibleContactAvatars = useCallback(async (usernames: string[]) => { + const targets = Array.from(new Set( + (usernames || []) + .map((username) => String(username || '').trim()) + .filter(Boolean) + )).filter((username) => { + if (avatarHydrationRequestedRef.current.has(username)) return false + const contact = contactsList.find((item) => item.username === username) + const session = sessions.find((item) => item.username === username) + const existingAvatarUrl = normalizeExportAvatarUrl(contact?.avatarUrl || session?.avatarUrl) + return !existingAvatarUrl + }) + + if (targets.length === 0) return + targets.forEach((username) => avatarHydrationRequestedRef.current.add(username)) + + const settled = await Promise.allSettled( + targets.map(async (username) => { + const profile = await window.electronAPI.chat.getContactAvatar(username) + return { + username, + avatarUrl: normalizeExportAvatarUrl(profile?.avatarUrl), + displayName: profile?.displayName ? String(profile.displayName).trim() : undefined + } + }) + ) + + const avatarPatches = new Map() + for (const item of settled) { + if (item.status !== 'fulfilled') continue + const { username, avatarUrl, displayName } = item.value + if (!avatarUrl && !displayName) continue + avatarPatches.set(username, { avatarUrl, displayName }) + } + if (avatarPatches.size === 0) return + + const now = Date.now() + setContactsList((prev) => prev.map((contact) => { + const patch = avatarPatches.get(contact.username) + if (!patch) return contact + return { + ...contact, + displayName: patch.displayName || contact.displayName, + avatarUrl: patch.avatarUrl || contact.avatarUrl + } + })) + setSessions((prev) => prev.map((session) => { + const patch = avatarPatches.get(session.username) + if (!patch) return session + return { + ...session, + displayName: patch.displayName || session.displayName, + avatarUrl: patch.avatarUrl || session.avatarUrl + } + })) + setSessionDetail((prev) => { + if (!prev) return prev + const patch = avatarPatches.get(prev.wxid) + if (!patch) return prev + return { + ...prev, + displayName: patch.displayName || prev.displayName, + avatarUrl: patch.avatarUrl || prev.avatarUrl + } + }) + + let avatarCacheChanged = false + for (const [username, patch] of avatarPatches.entries()) { + if (!patch.avatarUrl) continue + const previous = contactsAvatarCacheRef.current[username] + if (previous?.avatarUrl === patch.avatarUrl) continue + contactsAvatarCacheRef.current[username] = { + avatarUrl: patch.avatarUrl, + updatedAt: now, + checkedAt: now + } + avatarCacheChanged = true + } + if (avatarCacheChanged) { + setAvatarCacheUpdatedAt(now) + const scopeKey = exportCacheScopeRef.current + if (scopeKey) { + void configService.setContactsAvatarCache(scopeKey, contactsAvatarCacheRef.current).catch(() => {}) + } + } + }, [contactsList, sessions]) + + useEffect(() => { if (!isExportRoute) return let cancelled = false @@ -3824,10 +3923,12 @@ function ExportPage() { displayName: contact.displayName || contact.username, remark: contact.remark, nickname: contact.nickname, + alias: contact.alias, type: contact.type })) const persistAt = Date.now() + setContactsList(contactsForPersist) setSessions(nextSessions) sessionsHydratedAtRef.current = persistAt if (hasNetworkContactsSnapshot && contactsCachePayload.length > 0) { @@ -5380,6 +5481,11 @@ function ExportPage() { const endIndex = Number.isFinite(range?.endIndex) ? Math.max(startIndex, Math.floor(range.endIndex)) : startIndex sessionMediaMetricVisibleRangeRef.current = { startIndex, endIndex } sessionMutualFriendsVisibleRangeRef.current = { startIndex, endIndex } + void hydrateVisibleContactAvatars( + filteredContacts + .slice(startIndex, endIndex + 1) + .map((contact) => contact.username) + ) const visibleTargets = collectVisibleSessionMetricTargets(filteredContacts) if (visibleTargets.length === 0) return enqueueSessionMediaMetricRequests(visibleTargets, { front: true }) @@ -5395,10 +5501,23 @@ function ExportPage() { enqueueSessionMediaMetricRequests, enqueueSessionMutualFriendsRequests, filteredContacts, + hydrateVisibleContactAvatars, scheduleSessionMediaMetricWorker, scheduleSessionMutualFriendsWorker ]) + useEffect(() => { + if (filteredContacts.length === 0) return + const bootstrapTargets = filteredContacts.slice(0, 24).map((contact) => contact.username) + void hydrateVisibleContactAvatars(bootstrapTargets) + }, [filteredContacts, hydrateVisibleContactAvatars]) + + useEffect(() => { + const sessionId = String(sessionDetail?.wxid || '').trim() + if (!sessionId) return + void hydrateVisibleContactAvatars([sessionId]) + }, [hydrateVisibleContactAvatars, sessionDetail?.wxid]) + useEffect(() => { if (activeTaskCount > 0) return if (filteredContacts.length === 0) return @@ -5750,7 +5869,7 @@ function ExportPage() { displayName: mappedSession?.displayName || mappedContact?.displayName || prev?.displayName || normalizedSessionId, remark: sameSession ? prev?.remark : mappedContact?.remark, nickName: sameSession ? prev?.nickName : mappedContact?.nickname, - alias: sameSession ? prev?.alias : undefined, + alias: sameSession ? prev?.alias : mappedContact?.alias, avatarUrl: mappedSession?.avatarUrl || mappedContact?.avatarUrl || (sameSession ? prev?.avatarUrl : undefined), messageCount: initialMessageCount ?? (sameSession ? prev.messageCount : Number.NaN), voiceMessages: metricVoice ?? (sameSession ? prev?.voiceMessages : undefined), @@ -6627,11 +6746,12 @@ function ExportPage() {
- {contact.avatarUrl ? ( - - ) : ( - {getAvatarLetter(contact.displayName)} - )} +
{contact.displayName}
@@ -7514,11 +7634,12 @@ function ExportPage() {
- {sessionMutualFriendsDialogTarget.avatarUrl ? ( - - ) : ( - {getAvatarLetter(sessionMutualFriendsDialogTarget.displayName)} - )} +

{sessionMutualFriendsDialogTarget.displayName} 的共同好友

@@ -7599,11 +7720,12 @@ function ExportPage() {
- {sessionDetail?.avatarUrl ? ( - - ) : ( - {getAvatarLetter(sessionDetail?.displayName || sessionDetail?.wxid || '')} - )} +

{sessionDetail?.displayName || '会话详情'}

diff --git a/src/services/config.ts b/src/services/config.ts index 31f8f00..37c404f 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -662,6 +662,7 @@ export interface ContactsListCacheContact { displayName: string remark?: string nickname?: string + alias?: string type: 'friend' | 'group' | 'official' | 'former_friend' | 'other' } @@ -1174,6 +1175,7 @@ export async function getContactsListCache(scopeKey: string): Promise