修复朋友圈缓存问题
This commit is contained in:
1 parent
4daeb59b6b
commit
46b49ef305
5 files changed
+738
-35
No files matched your search
@@ -202,16 +202,16 @@ export function ContactSnsTimelineDialog({
|
||||
setTimelineStatsLoading(true)
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.sns.getUserPostCounts()
|
||||
const result = await window.electronAPI.sns.getUserPostStats(nextTarget.username)
|
||||
if (requestToken !== totalPostsRequestTokenRef.current) return
|
||||
|
||||
if (!result.success || !result.counts) {
|
||||
if (!result.success || !result.data) {
|
||||
setTimelineTotalPosts(null)
|
||||
setRankTotalPosts(null)
|
||||
return
|
||||
}
|
||||
|
||||
const rawCount = Number(result.counts[nextTarget.username] || 0)
|
||||
const rawCount = Number(result.data.totalPosts || 0)
|
||||
const normalized = Number.isFinite(rawCount) ? Math.max(0, Math.floor(rawCount)) : 0
|
||||
setTimelineTotalPosts(normalized)
|
||||
setRankTotalPosts(normalized)
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
:root {
|
||||
--sns-card-bg: transparent;
|
||||
--sns-border-radius-md: 8px;
|
||||
--sns-border-radius-sm: 6px;
|
||||
--sns-media-cell: 88px;
|
||||
}
|
||||
|
||||
.sns-post-item {
|
||||
background: var(--sns-card-bg);
|
||||
border: none;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow: visible;
|
||||
padding: 10px 6px 12px;
|
||||
position: relative;
|
||||
transition: background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--bg-secondary) 72%, transparent);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&.post-deleted {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.post-deleted-badge {
|
||||
align-items: center;
|
||||
background: rgba(255, 77, 79, 0.1);
|
||||
border-radius: 6px;
|
||||
color: #ff4d4f;
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
gap: 4px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-avatar-col {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.author-trigger-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.avatar-trigger {
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-content-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.post-header-row {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.post-author-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.author-name {
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.author-name-trigger {
|
||||
align-self: flex-start;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 2px;
|
||||
|
||||
.author-name {
|
||||
margin-bottom: 0;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: transparent;
|
||||
text-underline-offset: 2px;
|
||||
transition: color 0.15s ease, text-decoration-color 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover .author-name {
|
||||
color: var(--primary);
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
}
|
||||
|
||||
.post-header-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.post-time-standalone {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.debug-btn {
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 24px;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
transition: opacity 0.2s;
|
||||
width: 24px;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.delete-btn:hover {
|
||||
background: rgba(255, 77, 79, 0.08);
|
||||
border-color: rgba(255, 77, 79, 0.4);
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.post-protected-badge {
|
||||
align-items: center;
|
||||
background: rgba(76, 175, 80, 0.08);
|
||||
border: 1px solid rgba(76, 175, 80, 0.2);
|
||||
border-radius: 5px;
|
||||
color: var(--color-success, #4caf50);
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
gap: 3px;
|
||||
opacity: 0;
|
||||
padding: 3px 7px;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.sns-post-item:hover .debug-btn,
|
||||
.sns-post-item:hover .post-protected-badge {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.post-text {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
line-height: 1.48;
|
||||
margin: 0 0 8px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.post-location {
|
||||
align-items: flex-start;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
gap: 6px;
|
||||
line-height: 1.45;
|
||||
margin: -2px 0 8px;
|
||||
|
||||
svg {
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.post-location-text {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.post-media-container {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.post-link-card {
|
||||
align-items: center;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: rgba(var(--primary-rgb), 0.08);
|
||||
border-color: rgba(var(--primary-rgb), 0.3);
|
||||
}
|
||||
|
||||
.link-thumb {
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
flex-shrink: 0;
|
||||
height: 48px;
|
||||
overflow: hidden;
|
||||
width: 48px;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.link-thumb-fallback {
|
||||
align-items: center;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.link-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.link-title {
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.link-url {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-arrow {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.post-interactions {
|
||||
border-top: none;
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
padding-top: 0;
|
||||
|
||||
.likes-block {
|
||||
color: var(--primary);
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
gap: 6px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.like-icon {
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.comments-block {
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
padding: 6px 8px;
|
||||
|
||||
.comment-row {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.45;
|
||||
margin-bottom: 3px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-user {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.reply-text {
|
||||
color: var(--text-tertiary);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.comment-colon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.comment-custom-emoji {
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
margin-left: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sns-media-grid {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
|
||||
&.grid-1 .sns-media-item {
|
||||
aspect-ratio: auto;
|
||||
border-radius: var(--sns-border-radius-md);
|
||||
height: auto;
|
||||
max-height: 340px;
|
||||
max-width: 270px;
|
||||
width: fit-content;
|
||||
|
||||
&.deleted-media,
|
||||
&.failed-media {
|
||||
aspect-ratio: 1;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
display: block;
|
||||
height: auto;
|
||||
max-height: 340px;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.grid-2,
|
||||
&.grid-4 {
|
||||
grid-template-columns: repeat(2, var(--sns-media-cell));
|
||||
max-width: calc((var(--sns-media-cell) * 2) + 4px);
|
||||
}
|
||||
|
||||
&.grid-3,
|
||||
&.grid-6,
|
||||
&.grid-9 {
|
||||
grid-template-columns: repeat(3, var(--sns-media-cell));
|
||||
max-width: calc((var(--sns-media-cell) * 3) + 8px);
|
||||
}
|
||||
|
||||
.sns-media-item {
|
||||
aspect-ratio: 1;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: var(--sns-border-radius-sm);
|
||||
cursor: zoom-in;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
animation: snsPostFadeIn 0.2s ease;
|
||||
display: block;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media-badge {
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
left: 50%;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 32px;
|
||||
|
||||
&.live {
|
||||
height: 24px;
|
||||
left: auto;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
transform: none;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.media-decrypting-mask {
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
gap: 8px;
|
||||
inset: 0;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.media-download-btn {
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
bottom: 5px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 24px;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
transition: opacity 0.15s ease, background 0.15s ease;
|
||||
width: 24px;
|
||||
z-index: 5;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
inset: -4px;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .media-download-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.deleted-media {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
|
||||
.deleted-placeholder {
|
||||
align-items: center;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
&.failed-media {
|
||||
cursor: pointer;
|
||||
|
||||
.failed-placeholder {
|
||||
align-items: center;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
gap: 6px;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover .failed-placeholder {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes snsPostFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { SnsPost, SnsLinkCardData, SnsLocation } from '../../types/sns'
|
||||
import { Avatar } from '../Avatar'
|
||||
import { SnsMediaGrid } from './SnsMediaGrid'
|
||||
import { renderTextWithEmoji } from '../../utils/renderTextWithEmoji'
|
||||
import './SnsPostItem.scss'
|
||||
|
||||
// Helper functions (extracted from SnsPage.tsx but simplified/reused)
|
||||
const LINK_XML_URL_TAGS = ['url', 'shorturl', 'weburl', 'webpageurl', 'jumpurl']
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
serializeExportDateRangeConfig,
|
||||
type ExportDefaultDateRangeConfig
|
||||
} from '../../../../utils/exportDateRange'
|
||||
import type { ExportDialogState, ExportOptions, TextExportFormat } from '../../types'
|
||||
import type { DisplayNamePreference, ExportDialogState, ExportOptions, TextExportFormat } from '../../types'
|
||||
import { conflictStrategyOptions, displayNameOptions, formatOptions, MAX_EXPORT_FILE_SIZE_MB_LIMIT } from '../../constants'
|
||||
import { formatPathBrief } from '../../utils/format'
|
||||
import './ExportDialog.scss'
|
||||
@@ -65,6 +65,20 @@ const ExportDialog: React.FC<ExportDialogProps> = ({
|
||||
return getExportDateRangeLabel(currentSelection)
|
||||
}, [currentSelection])
|
||||
|
||||
const hasGroupSession = React.useMemo(() => {
|
||||
return dialogState.sessionIds.some(sessionId => String(sessionId || '').includes('@chatroom'))
|
||||
}, [dialogState.sessionIds])
|
||||
|
||||
const visibleDisplayNameOptions = React.useMemo(() => {
|
||||
return hasGroupSession
|
||||
? displayNameOptions
|
||||
: displayNameOptions.filter(item => item.value !== 'group-nickname')
|
||||
}, [hasGroupSession])
|
||||
|
||||
const effectiveDisplayNamePreference: DisplayNamePreference = !hasGroupSession && draftOptions.displayNamePreference === 'group-nickname'
|
||||
? 'remark'
|
||||
: draftOptions.displayNamePreference
|
||||
|
||||
if (!dialogState.open) return null
|
||||
|
||||
const { intent, scope, sessionNames, title } = dialogState
|
||||
@@ -320,12 +334,15 @@ const ExportDialog: React.FC<ExportDialogProps> = ({
|
||||
|
||||
<div className="display-name-control">
|
||||
<span>命名方式:</span>
|
||||
<div className="display-name-segmented" title="控制导出群消息时发送者名称的优先级">
|
||||
{displayNameOptions.map(item => (
|
||||
<div
|
||||
className="display-name-segmented"
|
||||
title={hasGroupSession ? '控制导出群消息时发送者名称的优先级' : '控制导出私聊消息时联系人名称的优先级'}
|
||||
>
|
||||
{visibleDisplayNameOptions.map(item => (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
className={draftOptions.displayNamePreference === item.value ? 'active' : ''}
|
||||
className={effectiveDisplayNamePreference === item.value ? 'active' : ''}
|
||||
title={item.desc}
|
||||
onClick={() => updateDraftOptions({ displayNamePreference: item.value })}
|
||||
>
|
||||
|
||||
+180
-28
@@ -21,7 +21,7 @@ import {
|
||||
getExportDateRangeLabel,
|
||||
type ExportDateRangeSelection
|
||||
} from '../utils/exportDateRange'
|
||||
import { displayNameForCompare, displayNameOrFallback } from '../utils/displayName'
|
||||
import { displayNameForCompare, displayNameOrFallback, pickDisplayName } from '../utils/displayName'
|
||||
|
||||
const SNS_PAGE_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
||||
const SNS_PAGE_CACHE_POST_LIMIT = 200
|
||||
@@ -158,6 +158,21 @@ const normalizeAccountId = (value?: string | null): string => {
|
||||
|
||||
const normalizeNameForCompare = (value?: string | null): string => displayNameForCompare(value)
|
||||
|
||||
const isRawWxidDisplayName = (value?: string | null): boolean => {
|
||||
const normalized = String(value || '').trim()
|
||||
return /^wxid_[a-z0-9_]+$/i.test(normalized)
|
||||
}
|
||||
|
||||
const pickMeaningfulContactDisplayName = (username: string, ...values: Array<unknown>): string | undefined => {
|
||||
const picked = pickDisplayName(...values)
|
||||
if (!picked) return undefined
|
||||
const normalizedPicked = normalizeAccountId(picked)
|
||||
const normalizedUsername = normalizeAccountId(username)
|
||||
if (normalizedPicked && normalizedUsername && normalizedPicked === normalizedUsername) return undefined
|
||||
if (isRawWxidDisplayName(picked)) return undefined
|
||||
return picked
|
||||
}
|
||||
|
||||
export default function SnsPage() {
|
||||
const [posts, setPosts] = useState<SnsPost[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -439,6 +454,38 @@ export default function SnsPage() {
|
||||
return [...input].sort(compareContactsForRanking)
|
||||
}, [compareContactsForRanking])
|
||||
|
||||
useEffect(() => {
|
||||
if (!contactsLoading || contactsRef.current.length > 0 || posts.length === 0) return
|
||||
|
||||
const fallbackMap = new Map<string, Contact>()
|
||||
for (const post of posts) {
|
||||
const username = String(post.username || '').trim()
|
||||
if (!username || username.endsWith('@chatroom') || username.startsWith('gh_') || fallbackMap.has(username)) {
|
||||
continue
|
||||
}
|
||||
const displayName = pickMeaningfulContactDisplayName(username, post.nickname)
|
||||
if (!displayName) continue
|
||||
fallbackMap.set(username, {
|
||||
username,
|
||||
displayName,
|
||||
avatarUrl: post.avatarUrl,
|
||||
type: 'friend',
|
||||
lastSessionTimestamp: Number(post.createTime || 0),
|
||||
postCountStatus: 'idle'
|
||||
})
|
||||
}
|
||||
|
||||
const fallbackContacts = sortContactsForRanking(Array.from(fallbackMap.values()))
|
||||
if (fallbackContacts.length === 0) return
|
||||
setContacts(fallbackContacts)
|
||||
setContactsLoading(false)
|
||||
setContactsCountProgress({
|
||||
resolved: 0,
|
||||
total: fallbackContacts.length,
|
||||
running: false
|
||||
})
|
||||
}, [contactsLoading, posts, sortContactsForRanking])
|
||||
|
||||
const resolvedCurrentUserContact = useMemo(() => {
|
||||
const normalizedWxid = normalizeAccountId(currentUserProfile.wxid)
|
||||
const normalizedAlias = normalizeAccountId(currentUserProfile.alias)
|
||||
@@ -1448,21 +1495,42 @@ export default function SnsPage() {
|
||||
setContactsLoading(true)
|
||||
try {
|
||||
const snsPostCountsScopeKey = await ensureSnsUserPostCountsCacheScopeKey()
|
||||
const [cachedPostCountsItem, cachedContactsItem, cachedAvatarItem] = await Promise.all([
|
||||
const snsPageCacheScopeKey = await ensureSnsCacheScopeKey()
|
||||
const [cachedPostCountsItem, cachedContactsItem, cachedAvatarItem, cachedSnsPageItem] = await Promise.all([
|
||||
configService.getExportSnsUserPostCountsCache(snsPostCountsScopeKey),
|
||||
configService.getContactsListCache(snsPostCountsScopeKey),
|
||||
configService.getContactsAvatarCache(snsPostCountsScopeKey)
|
||||
configService.getContactsAvatarCache(snsPostCountsScopeKey),
|
||||
configService.getSnsPageCache(snsPageCacheScopeKey)
|
||||
])
|
||||
const cachedPostCounts = forceCounts ? {} : (cachedPostCountsItem?.counts || {})
|
||||
const cachedAvatarMap = cachedAvatarItem?.avatars || {}
|
||||
const isCachedSnsPost = (post: unknown): post is SnsPost => {
|
||||
if (!post || typeof post !== 'object') return false
|
||||
const candidate = post as Partial<SnsPost>
|
||||
return typeof candidate.username === 'string' && typeof candidate.createTime === 'number'
|
||||
}
|
||||
const cachedPagePosts = Array.isArray(cachedSnsPageItem?.posts)
|
||||
? cachedSnsPageItem.posts.filter(isCachedSnsPost)
|
||||
: []
|
||||
const latestPostByUsername = new Map<string, SnsPost>()
|
||||
for (const post of [...cachedPagePosts, ...postsRef.current]) {
|
||||
const username = String(post.username || '').trim()
|
||||
if (!username) continue
|
||||
const previous = latestPostByUsername.get(username)
|
||||
if (!previous || Number(post.createTime || 0) > Number(previous.createTime || 0)) {
|
||||
latestPostByUsername.set(username, post)
|
||||
}
|
||||
}
|
||||
const cachedContacts = (cachedContactsItem?.contacts || [])
|
||||
.filter((contact) => contact.type === 'friend' || contact.type === 'former_friend')
|
||||
.map((contact) => {
|
||||
.map((contact): Contact | null => {
|
||||
const displayName = pickMeaningfulContactDisplayName(contact.username, contact.displayName, contact.remark, contact.nickname)
|
||||
if (!displayName) return null
|
||||
const cachedCount = cachedPostCounts[contact.username]
|
||||
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
||||
return {
|
||||
username: contact.username,
|
||||
displayName: displayNameOrFallback(contact.username, contact.displayName),
|
||||
displayName,
|
||||
avatarUrl: cachedAvatarMap[contact.username]?.avatarUrl,
|
||||
remark: contact.remark,
|
||||
nickname: contact.nickname,
|
||||
@@ -1476,6 +1544,36 @@ export default function SnsPage() {
|
||||
: 'idle' as ContactPostCountStatus
|
||||
}
|
||||
})
|
||||
.filter((contact): contact is Contact => Boolean(contact))
|
||||
const fallbackContactsFromSns = (() => {
|
||||
const map = new Map<string, Contact>()
|
||||
const addPost = (rawUsername: string, post?: SnsPost) => {
|
||||
const username = String(rawUsername || '').trim()
|
||||
if (!username || username.endsWith('@chatroom') || username.startsWith('gh_') || map.has(username)) return
|
||||
const displayName = pickMeaningfulContactDisplayName(username, post?.nickname)
|
||||
if (!displayName) return
|
||||
const cachedCount = cachedPostCounts[username]
|
||||
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
||||
map.set(username, {
|
||||
username,
|
||||
displayName,
|
||||
avatarUrl: post?.avatarUrl,
|
||||
type: 'friend',
|
||||
lastSessionTimestamp: Number(post?.createTime || 0),
|
||||
postCount: hasCachedCount ? Math.max(0, Math.floor(cachedCount)) : undefined,
|
||||
postCountStatus: hasCachedCount
|
||||
? 'ready' as ContactPostCountStatus
|
||||
: forceCounts
|
||||
? 'loading' as ContactPostCountStatus
|
||||
: 'idle' as ContactPostCountStatus
|
||||
})
|
||||
}
|
||||
|
||||
for (const [username, post] of latestPostByUsername.entries()) {
|
||||
addPost(username, post)
|
||||
}
|
||||
return Array.from(map.values())
|
||||
})()
|
||||
|
||||
if (requestToken !== contactsLoadTokenRef.current) {
|
||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||
@@ -1486,8 +1584,9 @@ export default function SnsPage() {
|
||||
})
|
||||
return
|
||||
}
|
||||
if (cachedContacts.length > 0) {
|
||||
const cachedContactsSorted = sortContactsForRanking(cachedContacts)
|
||||
const initialContacts = cachedContacts.length > 0 ? cachedContacts : fallbackContactsFromSns
|
||||
if (initialContacts.length > 0) {
|
||||
const cachedContactsSorted = sortContactsForRanking(initialContacts)
|
||||
setContacts(cachedContactsSorted)
|
||||
setContactsLoading(false)
|
||||
const cachedReadyCount = cachedContactsSorted.filter(contact => contact.postCountStatus === 'ready').length
|
||||
@@ -1502,9 +1601,10 @@ export default function SnsPage() {
|
||||
detail: '正在读取联系人与最近会话数据',
|
||||
progressText: '联系人快照'
|
||||
})
|
||||
const [contactsResult, sessionsResult] = await Promise.all([
|
||||
window.electronAPI.chat.getContacts(),
|
||||
window.electronAPI.chat.getSessions()
|
||||
const [contactsResult, sessionsResult, snsUsernamesResult] = await Promise.all([
|
||||
window.electronAPI.chat.getContacts({ lite: true }),
|
||||
window.electronAPI.chat.getSessions(),
|
||||
window.electronAPI.sns.getSnsUsernames()
|
||||
])
|
||||
if (isBackgroundTaskCancelRequested(taskId)) {
|
||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||
@@ -1536,12 +1636,14 @@ export default function SnsPage() {
|
||||
if (contactsResult.success && contactsResult.contacts) {
|
||||
for (const c of contactsResult.contacts) {
|
||||
if (c.type === 'friend' || c.type === 'former_friend') {
|
||||
const displayName = pickMeaningfulContactDisplayName(c.username, c.displayName, c.remark, c.nickname)
|
||||
if (!displayName) continue
|
||||
const cachedCount = cachedPostCounts[c.username]
|
||||
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
||||
contactMap.set(c.username, {
|
||||
username: c.username,
|
||||
displayName: c.displayName,
|
||||
avatarUrl: c.avatarUrl,
|
||||
displayName,
|
||||
avatarUrl: c.avatarUrl || cachedAvatarMap[c.username]?.avatarUrl,
|
||||
remark: c.remark,
|
||||
nickname: c.nickname,
|
||||
type: c.type === 'former_friend' ? 'former_friend' : 'friend',
|
||||
@@ -1553,6 +1655,17 @@ export default function SnsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const snsOnlyUsernames: string[] = []
|
||||
if (snsUsernamesResult.success && Array.isArray(snsUsernamesResult.usernames)) {
|
||||
for (const rawUsername of snsUsernamesResult.usernames) {
|
||||
const username = String(rawUsername || '').trim()
|
||||
if (!username || username.endsWith('@chatroom') || username.startsWith('gh_') || contactMap.has(username)) {
|
||||
continue
|
||||
}
|
||||
snsOnlyUsernames.push(username)
|
||||
}
|
||||
}
|
||||
|
||||
let contactsList = sortContactsForRanking(Array.from(contactMap.values()))
|
||||
if (requestToken !== contactsLoadTokenRef.current) {
|
||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||
@@ -1563,18 +1676,11 @@ export default function SnsPage() {
|
||||
})
|
||||
return
|
||||
}
|
||||
setContacts(contactsList)
|
||||
const readyUsernames = new Set(
|
||||
contactsList
|
||||
.filter((contact) => contact.postCountStatus === 'ready' && typeof contact.postCount === 'number')
|
||||
.map((contact) => contact.username)
|
||||
)
|
||||
void hydrateContactPostCounts(
|
||||
contactsList.map(contact => contact.username),
|
||||
{ readyUsernames, force: forceCounts }
|
||||
)
|
||||
|
||||
const allUsernames = contactsList.map(c => c.username)
|
||||
const allUsernames = Array.from(new Set([
|
||||
...contactsList.map(c => c.username),
|
||||
...snsOnlyUsernames
|
||||
]))
|
||||
|
||||
// 用 enrichSessionsContactInfo 统一补充头像和显示名
|
||||
if (allUsernames.length > 0) {
|
||||
@@ -1593,15 +1699,44 @@ export default function SnsPage() {
|
||||
return
|
||||
}
|
||||
if (enriched.success && enriched.contacts) {
|
||||
const nextContactMap = new Map<string, Contact>()
|
||||
contactsList = contactsList.map((contact) => {
|
||||
const extra = enriched.contacts?.[contact.username]
|
||||
if (!extra) return contact
|
||||
return {
|
||||
if (!extra) {
|
||||
nextContactMap.set(contact.username, contact)
|
||||
return contact
|
||||
}
|
||||
const displayName = pickMeaningfulContactDisplayName(contact.username, extra.displayName, contact.displayName)
|
||||
const nextContact = {
|
||||
...contact,
|
||||
displayName: displayNameOrFallback(contact.displayName, extra.displayName),
|
||||
displayName: displayName || contact.displayName,
|
||||
avatarUrl: extra.avatarUrl || contact.avatarUrl
|
||||
}
|
||||
nextContactMap.set(contact.username, nextContact)
|
||||
return nextContact
|
||||
})
|
||||
|
||||
for (const username of snsOnlyUsernames) {
|
||||
if (nextContactMap.has(username)) continue
|
||||
const extra = enriched.contacts[username]
|
||||
const displayName = pickMeaningfulContactDisplayName(username, extra?.displayName)
|
||||
if (!displayName) continue
|
||||
const cachedCount = cachedPostCounts[username]
|
||||
const hasCachedCount = typeof cachedCount === 'number' && Number.isFinite(cachedCount)
|
||||
nextContactMap.set(username, {
|
||||
username,
|
||||
displayName,
|
||||
avatarUrl: extra?.avatarUrl || cachedAvatarMap[username]?.avatarUrl,
|
||||
type: 'friend',
|
||||
lastSessionTimestamp: Number(sessionTimestampMap.get(username) || 0),
|
||||
postCount: hasCachedCount ? Math.max(0, Math.floor(cachedCount)) : undefined,
|
||||
postCountStatus: hasCachedCount ? 'ready' : (forceCounts ? 'loading' : 'idle')
|
||||
})
|
||||
}
|
||||
|
||||
contactsList = sortContactsForRanking(Array.from(nextContactMap.values()).filter(contact => (
|
||||
Boolean(pickMeaningfulContactDisplayName(contact.username, contact.displayName, contact.remark, contact.nickname))
|
||||
)))
|
||||
if (requestToken !== contactsLoadTokenRef.current) {
|
||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||
activeContactsLoadTaskIdRef.current = null
|
||||
@@ -1618,13 +1753,30 @@ export default function SnsPage() {
|
||||
return {
|
||||
...contact,
|
||||
lastSessionTimestamp: previous?.lastSessionTimestamp ?? contact.lastSessionTimestamp,
|
||||
postCount: previous?.postCount,
|
||||
postCount: previous?.postCount ?? contact.postCount,
|
||||
postCountStatus: previous?.postCountStatus ?? contact.postCountStatus
|
||||
}
|
||||
})
|
||||
return sortContactsForRanking(merged)
|
||||
})
|
||||
} else if (contactsList.length > 0) {
|
||||
setContacts(contactsList)
|
||||
}
|
||||
} else if (contactsList.length > 0) {
|
||||
setContacts(contactsList)
|
||||
}
|
||||
|
||||
setContactsLoading(false)
|
||||
const readyUsernames = new Set(
|
||||
contactsList
|
||||
.filter((contact) => contact.postCountStatus === 'ready' && typeof contact.postCount === 'number')
|
||||
.map((contact) => contact.username)
|
||||
)
|
||||
if (contactsList.length > 0) {
|
||||
void hydrateContactPostCounts(
|
||||
contactsList.map(contact => contact.username),
|
||||
{ readyUsernames, force: forceCounts }
|
||||
)
|
||||
}
|
||||
if (activeContactsLoadTaskIdRef.current === taskId) {
|
||||
activeContactsLoadTaskIdRef.current = null
|
||||
@@ -1659,7 +1811,7 @@ export default function SnsPage() {
|
||||
setContactsLoading(false)
|
||||
}
|
||||
}
|
||||
}, [ensureSnsUserPostCountsCacheScopeKey, hydrateContactPostCounts, sortContactsForRanking, stopContactsCountHydration])
|
||||
}, [ensureSnsCacheScopeKey, ensureSnsUserPostCountsCacheScopeKey, hydrateContactPostCounts, sortContactsForRanking, stopContactsCountHydration])
|
||||
|
||||
const closeAuthorTimeline = useCallback(() => {
|
||||
setAuthorTimelineTarget(null)
|
||||
|
||||
Reference in new issue
Block a user