修复朋友圈缓存问题

This commit is contained in:
cc committed 2026-07-07 14:58:09 +08:00
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)
+533
View File
@@ -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;
}
}
+1
View File
@@ -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
View File
@@ -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)