mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 15:25:50 +00:00
feat(sns): cache page data and show count loading state
This commit is contained in:
@@ -8,6 +8,7 @@ interface Contact {
|
|||||||
displayName: string
|
displayName: string
|
||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
postCount?: number
|
postCount?: number
|
||||||
|
postCountStatus?: 'loading' | 'ready' | 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnsFilterPanelProps {
|
interface SnsFilterPanelProps {
|
||||||
@@ -58,6 +59,16 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
setJumpTargetDate(undefined)
|
setJumpTargetDate(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPostCountDisplay = (contact: Contact) => {
|
||||||
|
if (contact.postCountStatus === 'error') {
|
||||||
|
return { text: '统计失败', className: 'is-error' }
|
||||||
|
}
|
||||||
|
if (contact.postCountStatus !== 'ready') {
|
||||||
|
return { text: '统计中', className: 'is-loading' }
|
||||||
|
}
|
||||||
|
return { text: `${Math.max(0, Number(contact.postCount || 0))} 条`, className: '' }
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="sns-filter-panel">
|
<aside className="sns-filter-panel">
|
||||||
<div className="filter-header">
|
<div className="filter-header">
|
||||||
@@ -144,7 +155,9 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="contact-list-scroll">
|
<div className="contact-list-scroll">
|
||||||
{filteredContacts.map(contact => (
|
{filteredContacts.map(contact => {
|
||||||
|
const countDisplay = getPostCountDisplay(contact)
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={contact.username}
|
key={contact.username}
|
||||||
className={`contact-row ${selectedUsernames.includes(contact.username) ? 'selected' : ''}`}
|
className={`contact-row ${selectedUsernames.includes(contact.username) ? 'selected' : ''}`}
|
||||||
@@ -153,10 +166,11 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
<Avatar src={contact.avatarUrl} name={contact.displayName} size={36} shape="rounded" />
|
<Avatar src={contact.avatarUrl} name={contact.displayName} size={36} shape="rounded" />
|
||||||
<div className="contact-meta">
|
<div className="contact-meta">
|
||||||
<span className="contact-name">{contact.displayName}</span>
|
<span className="contact-name">{contact.displayName}</span>
|
||||||
<span className="contact-post-count">{Math.max(0, Number(contact.postCount || 0))} 条</span>
|
<span className={`contact-post-count ${countDisplay.className}`}>{countDisplay.text}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
{filteredContacts.length === 0 && (
|
{filteredContacts.length === 0 && (
|
||||||
<div className="empty-state">没有找到联系人</div>
|
<div className="empty-state">没有找到联系人</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1113,6 +1113,14 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
|
|
||||||
|
&.is-loading {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-error {
|
||||||
|
color: var(--color-error, #f44336);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ import './SnsPage.scss'
|
|||||||
import { SnsPost } from '../types/sns'
|
import { SnsPost } from '../types/sns'
|
||||||
import { SnsPostItem } from '../components/Sns/SnsPostItem'
|
import { SnsPostItem } from '../components/Sns/SnsPostItem'
|
||||||
import { SnsFilterPanel } from '../components/Sns/SnsFilterPanel'
|
import { SnsFilterPanel } from '../components/Sns/SnsFilterPanel'
|
||||||
|
import * as configService from '../services/config'
|
||||||
|
|
||||||
|
const SNS_PAGE_CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
||||||
|
const SNS_PAGE_CACHE_POST_LIMIT = 200
|
||||||
|
const SNS_PAGE_CACHE_SCOPE_FALLBACK = '__default__'
|
||||||
|
|
||||||
interface Contact {
|
interface Contact {
|
||||||
username: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
type?: 'friend' | 'former_friend' | 'sns_only'
|
type?: 'friend' | 'former_friend' | 'sns_only'
|
||||||
postCount: number
|
postCount?: number
|
||||||
|
postCountStatus: 'loading' | 'ready' | 'error'
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SnsOverviewStats {
|
interface SnsOverviewStats {
|
||||||
@@ -71,12 +77,30 @@ export default function SnsPage() {
|
|||||||
const [hasNewer, setHasNewer] = useState(false)
|
const [hasNewer, setHasNewer] = useState(false)
|
||||||
const [loadingNewer, setLoadingNewer] = useState(false)
|
const [loadingNewer, setLoadingNewer] = useState(false)
|
||||||
const postsRef = useRef<SnsPost[]>([])
|
const postsRef = useRef<SnsPost[]>([])
|
||||||
|
const overviewStatsRef = useRef<SnsOverviewStats>(overviewStats)
|
||||||
|
const selectedUsernamesRef = useRef<string[]>(selectedUsernames)
|
||||||
|
const searchKeywordRef = useRef(searchKeyword)
|
||||||
|
const jumpTargetDateRef = useRef<Date | undefined>(jumpTargetDate)
|
||||||
|
const cacheScopeKeyRef = useRef('')
|
||||||
const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null)
|
const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null)
|
||||||
|
const contactsLoadTokenRef = useRef(0)
|
||||||
|
|
||||||
// Sync posts ref
|
// Sync posts ref
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
postsRef.current = posts
|
postsRef.current = posts
|
||||||
}, [posts])
|
}, [posts])
|
||||||
|
useEffect(() => {
|
||||||
|
overviewStatsRef.current = overviewStats
|
||||||
|
}, [overviewStats])
|
||||||
|
useEffect(() => {
|
||||||
|
selectedUsernamesRef.current = selectedUsernames
|
||||||
|
}, [selectedUsernames])
|
||||||
|
useEffect(() => {
|
||||||
|
searchKeywordRef.current = searchKeyword
|
||||||
|
}, [searchKeyword])
|
||||||
|
useEffect(() => {
|
||||||
|
jumpTargetDateRef.current = jumpTargetDate
|
||||||
|
}, [jumpTargetDate])
|
||||||
// 在 DOM 更新后、浏览器绘制前同步调整滚动位置,防止向上加载时页面跳动
|
// 在 DOM 更新后、浏览器绘制前同步调整滚动位置,防止向上加载时页面跳动
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const snapshot = scrollAdjustmentRef.current;
|
const snapshot = scrollAdjustmentRef.current;
|
||||||
@@ -100,6 +124,78 @@ export default function SnsPage() {
|
|||||||
return `${year}-${month}-${day}`
|
return `${year}-${month}-${day}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDefaultViewNow = useCallback(() => {
|
||||||
|
return selectedUsernamesRef.current.length === 0 && !searchKeywordRef.current.trim() && !jumpTargetDateRef.current
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const ensureSnsCacheScopeKey = useCallback(async () => {
|
||||||
|
if (cacheScopeKeyRef.current) return cacheScopeKeyRef.current
|
||||||
|
const wxid = (await configService.getMyWxid())?.trim() || SNS_PAGE_CACHE_SCOPE_FALLBACK
|
||||||
|
const scopeKey = `sns_page:${wxid}`
|
||||||
|
cacheScopeKeyRef.current = scopeKey
|
||||||
|
return scopeKey
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const persistSnsPageCache = useCallback(async (patch?: { posts?: SnsPost[]; overviewStats?: SnsOverviewStats }) => {
|
||||||
|
if (!isDefaultViewNow()) return
|
||||||
|
try {
|
||||||
|
const scopeKey = await ensureSnsCacheScopeKey()
|
||||||
|
if (!scopeKey) return
|
||||||
|
let postsToStore = patch?.posts ?? postsRef.current
|
||||||
|
if (!patch?.posts && postsToStore.length === 0) {
|
||||||
|
const existingCache = await configService.getSnsPageCache(scopeKey)
|
||||||
|
if (existingCache && Array.isArray(existingCache.posts) && existingCache.posts.length > 0) {
|
||||||
|
postsToStore = existingCache.posts as SnsPost[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const overviewToStore = patch?.overviewStats ?? overviewStatsRef.current
|
||||||
|
await configService.setSnsPageCache(scopeKey, {
|
||||||
|
overviewStats: overviewToStore,
|
||||||
|
posts: postsToStore.slice(0, SNS_PAGE_CACHE_POST_LIMIT)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to persist SNS page cache:', error)
|
||||||
|
}
|
||||||
|
}, [ensureSnsCacheScopeKey, isDefaultViewNow])
|
||||||
|
|
||||||
|
const hydrateSnsPageCache = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const scopeKey = await ensureSnsCacheScopeKey()
|
||||||
|
const cached = await configService.getSnsPageCache(scopeKey)
|
||||||
|
if (!cached) return
|
||||||
|
if (Date.now() - cached.updatedAt > SNS_PAGE_CACHE_TTL_MS) return
|
||||||
|
|
||||||
|
const cachedOverview = cached.overviewStats
|
||||||
|
if (cachedOverview) {
|
||||||
|
setOverviewStats({
|
||||||
|
totalPosts: Math.max(0, Number(cachedOverview.totalPosts || 0)),
|
||||||
|
totalFriends: Math.max(0, Number(cachedOverview.totalFriends || 0)),
|
||||||
|
earliestTime: cachedOverview.earliestTime ?? null,
|
||||||
|
latestTime: cachedOverview.latestTime ?? null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(cached.posts) && cached.posts.length > 0) {
|
||||||
|
const cachedPosts = cached.posts
|
||||||
|
.filter((raw): raw is SnsPost => {
|
||||||
|
if (!raw || typeof raw !== 'object') return false
|
||||||
|
const row = raw as Record<string, unknown>
|
||||||
|
return typeof row.id === 'string' && typeof row.createTime === 'number'
|
||||||
|
})
|
||||||
|
.slice(0, SNS_PAGE_CACHE_POST_LIMIT)
|
||||||
|
.sort((a, b) => b.createTime - a.createTime)
|
||||||
|
|
||||||
|
if (cachedPosts.length > 0) {
|
||||||
|
setPosts(cachedPosts)
|
||||||
|
setHasMore(true)
|
||||||
|
setHasNewer(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to hydrate SNS page cache:', error)
|
||||||
|
}
|
||||||
|
}, [ensureSnsCacheScopeKey])
|
||||||
|
|
||||||
const loadOverviewStats = useCallback(async () => {
|
const loadOverviewStats = useCallback(async () => {
|
||||||
setOverviewStatsLoading(true)
|
setOverviewStatsLoading(true)
|
||||||
try {
|
try {
|
||||||
@@ -129,18 +225,20 @@ export default function SnsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOverviewStats({
|
const nextOverviewStats = {
|
||||||
totalPosts,
|
totalPosts,
|
||||||
totalFriends,
|
totalFriends,
|
||||||
earliestTime,
|
earliestTime,
|
||||||
latestTime
|
latestTime
|
||||||
})
|
}
|
||||||
|
setOverviewStats(nextOverviewStats)
|
||||||
|
void persistSnsPageCache({ overviewStats: nextOverviewStats })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load SNS overview stats:', error)
|
console.error('Failed to load SNS overview stats:', error)
|
||||||
} finally {
|
} finally {
|
||||||
setOverviewStatsLoading(false)
|
setOverviewStatsLoading(false)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [persistSnsPageCache])
|
||||||
|
|
||||||
const loadPosts = useCallback(async (options: { reset?: boolean, direction?: 'older' | 'newer' } = {}) => {
|
const loadPosts = useCallback(async (options: { reset?: boolean, direction?: 'older' | 'newer' } = {}) => {
|
||||||
const { reset = false, direction = 'older' } = options
|
const { reset = false, direction = 'older' } = options
|
||||||
@@ -186,7 +284,9 @@ export default function SnsPage() {
|
|||||||
const uniqueNewer = result.timeline.filter((p: SnsPost) => !existingIds.has(p.id));
|
const uniqueNewer = result.timeline.filter((p: SnsPost) => !existingIds.has(p.id));
|
||||||
|
|
||||||
if (uniqueNewer.length > 0) {
|
if (uniqueNewer.length > 0) {
|
||||||
setPosts(prev => [...uniqueNewer, ...prev].sort((a, b) => b.createTime - a.createTime));
|
const merged = [...uniqueNewer, ...currentPosts].sort((a, b) => b.createTime - a.createTime)
|
||||||
|
setPosts(merged);
|
||||||
|
void persistSnsPageCache({ posts: merged })
|
||||||
}
|
}
|
||||||
setHasNewer(result.timeline.length >= limit);
|
setHasNewer(result.timeline.length >= limit);
|
||||||
} else {
|
} else {
|
||||||
@@ -216,6 +316,7 @@ export default function SnsPage() {
|
|||||||
if (result.success && result.timeline) {
|
if (result.success && result.timeline) {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
setPosts(result.timeline)
|
setPosts(result.timeline)
|
||||||
|
void persistSnsPageCache({ posts: result.timeline })
|
||||||
setHasMore(result.timeline.length >= limit)
|
setHasMore(result.timeline.length >= limit)
|
||||||
|
|
||||||
// Check for newer items above topTs
|
// Check for newer items above topTs
|
||||||
@@ -232,7 +333,9 @@ export default function SnsPage() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (result.timeline.length > 0) {
|
if (result.timeline.length > 0) {
|
||||||
setPosts(prev => [...prev, ...result.timeline!].sort((a, b) => b.createTime - a.createTime))
|
const merged = [...postsRef.current, ...result.timeline!].sort((a, b) => b.createTime - a.createTime)
|
||||||
|
setPosts(merged)
|
||||||
|
void persistSnsPageCache({ posts: merged })
|
||||||
}
|
}
|
||||||
if (result.timeline.length < limit) {
|
if (result.timeline.length < limit) {
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
@@ -246,22 +349,18 @@ export default function SnsPage() {
|
|||||||
setLoadingNewer(false)
|
setLoadingNewer(false)
|
||||||
loadingRef.current = false
|
loadingRef.current = false
|
||||||
}
|
}
|
||||||
}, [selectedUsernames, searchKeyword, jumpTargetDate])
|
}, [jumpTargetDate, persistSnsPageCache, searchKeyword, selectedUsernames])
|
||||||
|
|
||||||
// Load Contacts(合并好友+曾经好友+朋友圈发布者,enrichSessionsContactInfo 补充头像)
|
// Load Contacts(合并好友+曾经好友+朋友圈发布者,enrichSessionsContactInfo 补充头像)
|
||||||
const loadContacts = useCallback(async () => {
|
const loadContacts = useCallback(async () => {
|
||||||
|
const requestToken = ++contactsLoadTokenRef.current
|
||||||
setContactsLoading(true)
|
setContactsLoading(true)
|
||||||
try {
|
try {
|
||||||
// 并行获取联系人列表、朋友圈发布者列表和每个发布者的动态条数
|
// 先加载联系人基础信息,再异步补齐朋友圈条数
|
||||||
const [contactsResult, snsResult, snsCountsResult] = await Promise.all([
|
const [contactsResult, snsResult] = await Promise.all([
|
||||||
window.electronAPI.chat.getContacts(),
|
window.electronAPI.chat.getContacts(),
|
||||||
window.electronAPI.sns.getSnsUsernames(),
|
window.electronAPI.sns.getSnsUsernames()
|
||||||
window.electronAPI.sns.getUserPostCounts()
|
|
||||||
])
|
])
|
||||||
const snsPostCountMap = new Map<string, number>(
|
|
||||||
Object.entries(snsCountsResult.success ? (snsCountsResult.data || {}) : {})
|
|
||||||
.map(([username, count]) => [username, Math.max(0, Number(count || 0))])
|
|
||||||
)
|
|
||||||
|
|
||||||
// 以联系人为基础,按 username 去重
|
// 以联系人为基础,按 username 去重
|
||||||
const contactMap = new Map<string, Contact>()
|
const contactMap = new Map<string, Contact>()
|
||||||
@@ -275,7 +374,7 @@ export default function SnsPage() {
|
|||||||
displayName: c.displayName,
|
displayName: c.displayName,
|
||||||
avatarUrl: c.avatarUrl,
|
avatarUrl: c.avatarUrl,
|
||||||
type: c.type === 'former_friend' ? 'former_friend' : 'friend',
|
type: c.type === 'former_friend' ? 'former_friend' : 'friend',
|
||||||
postCount: snsPostCountMap.get(c.username) || 0
|
postCountStatus: 'loading'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,7 +384,7 @@ export default function SnsPage() {
|
|||||||
if (snsResult.success && snsResult.usernames) {
|
if (snsResult.success && snsResult.usernames) {
|
||||||
for (const u of snsResult.usernames) {
|
for (const u of snsResult.usernames) {
|
||||||
if (!contactMap.has(u)) {
|
if (!contactMap.has(u)) {
|
||||||
contactMap.set(u, { username: u, displayName: u, type: 'sns_only', postCount: snsPostCountMap.get(u) || 0 })
|
contactMap.set(u, { username: u, displayName: u, type: 'sns_only', postCountStatus: 'loading' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,32 +405,63 @@ export default function SnsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (requestToken !== contactsLoadTokenRef.current) return
|
||||||
setContacts(Array.from(contactMap.values()))
|
setContacts(Array.from(contactMap.values()))
|
||||||
|
|
||||||
|
const snsCountsResult = await window.electronAPI.sns.getUserPostCounts()
|
||||||
|
if (requestToken !== contactsLoadTokenRef.current) return
|
||||||
|
|
||||||
|
if (snsCountsResult.success && snsCountsResult.data) {
|
||||||
|
const snsPostCountMap = new Map<string, number>(
|
||||||
|
Object.entries(snsCountsResult.data).map(([username, count]) => [username, Math.max(0, Number(count || 0))])
|
||||||
|
)
|
||||||
|
setContacts(prev => prev.map(contact => ({
|
||||||
|
...contact,
|
||||||
|
postCount: snsPostCountMap.get(contact.username) ?? 0,
|
||||||
|
postCountStatus: 'ready'
|
||||||
|
})))
|
||||||
|
} else {
|
||||||
|
console.error('Failed to load SNS contact post counts:', snsCountsResult.error)
|
||||||
|
setContacts(prev => prev.map(contact => ({
|
||||||
|
...contact,
|
||||||
|
postCountStatus: 'error'
|
||||||
|
})))
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (requestToken !== contactsLoadTokenRef.current) return
|
||||||
console.error('Failed to load contacts:', error)
|
console.error('Failed to load contacts:', error)
|
||||||
|
setContacts(prev => prev.map(contact => ({
|
||||||
|
...contact,
|
||||||
|
postCountStatus: 'error'
|
||||||
|
})))
|
||||||
} finally {
|
} finally {
|
||||||
setContactsLoading(false)
|
if (requestToken === contactsLoadTokenRef.current) {
|
||||||
|
setContactsLoading(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Initial Load & Listeners
|
// Initial Load & Listeners
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
void hydrateSnsPageCache()
|
||||||
loadContacts()
|
loadContacts()
|
||||||
loadOverviewStats()
|
loadOverviewStats()
|
||||||
}, [loadContacts, loadOverviewStats])
|
}, [hydrateSnsPageCache, loadContacts, loadOverviewStats])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleChange = () => {
|
const handleChange = () => {
|
||||||
|
cacheScopeKeyRef.current = ''
|
||||||
// wxid changed, reset everything
|
// wxid changed, reset everything
|
||||||
setPosts([]); setHasMore(true); setHasNewer(false);
|
setPosts([]); setHasMore(true); setHasNewer(false);
|
||||||
setSelectedUsernames([]); setSearchKeyword(''); setJumpTargetDate(undefined);
|
setSelectedUsernames([]); setSearchKeyword(''); setJumpTargetDate(undefined);
|
||||||
|
void hydrateSnsPageCache()
|
||||||
loadContacts();
|
loadContacts();
|
||||||
loadOverviewStats();
|
loadOverviewStats();
|
||||||
loadPosts({ reset: true });
|
loadPosts({ reset: true });
|
||||||
}
|
}
|
||||||
window.addEventListener('wxid-changed', handleChange as EventListener)
|
window.addEventListener('wxid-changed', handleChange as EventListener)
|
||||||
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
|
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
|
||||||
}, [loadContacts, loadOverviewStats, loadPosts])
|
}, [hydrateSnsPageCache, loadContacts, loadOverviewStats, loadPosts])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -445,7 +575,11 @@ export default function SnsPage() {
|
|||||||
}}
|
}}
|
||||||
onDebug={(p) => setDebugPost(p)}
|
onDebug={(p) => setDebugPost(p)}
|
||||||
onDelete={(postId) => {
|
onDelete={(postId) => {
|
||||||
setPosts(prev => prev.filter(p => p.id !== postId))
|
setPosts(prev => {
|
||||||
|
const next = prev.filter(p => p.id !== postId)
|
||||||
|
void persistSnsPageCache({ posts: next })
|
||||||
|
return next
|
||||||
|
})
|
||||||
loadOverviewStats()
|
loadOverviewStats()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const CONFIG_KEYS = {
|
|||||||
EXPORT_LAST_SNS_POST_COUNT: 'exportLastSnsPostCount',
|
EXPORT_LAST_SNS_POST_COUNT: 'exportLastSnsPostCount',
|
||||||
EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP: 'exportSessionMessageCountCacheMap',
|
EXPORT_SESSION_MESSAGE_COUNT_CACHE_MAP: 'exportSessionMessageCountCacheMap',
|
||||||
EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap',
|
EXPORT_SNS_STATS_CACHE_MAP: 'exportSnsStatsCacheMap',
|
||||||
|
SNS_PAGE_CACHE_MAP: 'snsPageCacheMap',
|
||||||
CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs',
|
CONTACTS_LOAD_TIMEOUT_MS: 'contactsLoadTimeoutMs',
|
||||||
CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap',
|
CONTACTS_LIST_CACHE_MAP: 'contactsListCacheMap',
|
||||||
CONTACTS_AVATAR_CACHE_MAP: 'contactsAvatarCacheMap',
|
CONTACTS_AVATAR_CACHE_MAP: 'contactsAvatarCacheMap',
|
||||||
@@ -465,6 +466,19 @@ export interface ExportSnsStatsCacheItem {
|
|||||||
totalFriends: number
|
totalFriends: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SnsPageOverviewCache {
|
||||||
|
totalPosts: number
|
||||||
|
totalFriends: number
|
||||||
|
earliestTime: number | null
|
||||||
|
latestTime: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SnsPageCacheItem {
|
||||||
|
updatedAt: number
|
||||||
|
overviewStats: SnsPageOverviewCache
|
||||||
|
posts: unknown[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContactsListCacheContact {
|
export interface ContactsListCacheContact {
|
||||||
username: string
|
username: string
|
||||||
displayName: string
|
displayName: string
|
||||||
@@ -576,6 +590,70 @@ export async function setExportSnsStatsCache(
|
|||||||
await config.set(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP, map)
|
await config.set(CONFIG_KEYS.EXPORT_SNS_STATS_CACHE_MAP, map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSnsPageCache(scopeKey: string): Promise<SnsPageCacheItem | null> {
|
||||||
|
if (!scopeKey) return null
|
||||||
|
const value = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP)
|
||||||
|
if (!value || typeof value !== 'object') return null
|
||||||
|
const rawMap = value as Record<string, unknown>
|
||||||
|
const rawItem = rawMap[scopeKey]
|
||||||
|
if (!rawItem || typeof rawItem !== 'object') return null
|
||||||
|
|
||||||
|
const raw = rawItem as Record<string, unknown>
|
||||||
|
const rawOverview = raw.overviewStats
|
||||||
|
const rawPosts = raw.posts
|
||||||
|
if (!rawOverview || typeof rawOverview !== 'object' || !Array.isArray(rawPosts)) return null
|
||||||
|
|
||||||
|
const overviewObj = rawOverview as Record<string, unknown>
|
||||||
|
const normalizeNumber = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? Math.floor(v) : 0)
|
||||||
|
const normalizeNullableTimestamp = (v: unknown) => {
|
||||||
|
if (v === null || v === undefined) return null
|
||||||
|
if (typeof v === 'number' && Number.isFinite(v) && v > 0) return Math.floor(v)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updatedAt: typeof raw.updatedAt === 'number' && Number.isFinite(raw.updatedAt) ? raw.updatedAt : 0,
|
||||||
|
overviewStats: {
|
||||||
|
totalPosts: Math.max(0, normalizeNumber(overviewObj.totalPosts)),
|
||||||
|
totalFriends: Math.max(0, normalizeNumber(overviewObj.totalFriends)),
|
||||||
|
earliestTime: normalizeNullableTimestamp(overviewObj.earliestTime),
|
||||||
|
latestTime: normalizeNullableTimestamp(overviewObj.latestTime)
|
||||||
|
},
|
||||||
|
posts: rawPosts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setSnsPageCache(
|
||||||
|
scopeKey: string,
|
||||||
|
payload: { overviewStats: SnsPageOverviewCache; posts: unknown[] }
|
||||||
|
): Promise<void> {
|
||||||
|
if (!scopeKey) return
|
||||||
|
const current = await config.get(CONFIG_KEYS.SNS_PAGE_CACHE_MAP)
|
||||||
|
const map = current && typeof current === 'object'
|
||||||
|
? { ...(current as Record<string, unknown>) }
|
||||||
|
: {}
|
||||||
|
|
||||||
|
const normalizeNumber = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.floor(v)) : 0)
|
||||||
|
const normalizeNullableTimestamp = (v: unknown) => {
|
||||||
|
if (v === null || v === undefined) return null
|
||||||
|
if (typeof v === 'number' && Number.isFinite(v) && v > 0) return Math.floor(v)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
map[scopeKey] = {
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
overviewStats: {
|
||||||
|
totalPosts: normalizeNumber(payload?.overviewStats?.totalPosts),
|
||||||
|
totalFriends: normalizeNumber(payload?.overviewStats?.totalFriends),
|
||||||
|
earliestTime: normalizeNullableTimestamp(payload?.overviewStats?.earliestTime),
|
||||||
|
latestTime: normalizeNullableTimestamp(payload?.overviewStats?.latestTime)
|
||||||
|
},
|
||||||
|
posts: Array.isArray(payload?.posts) ? payload.posts : []
|
||||||
|
}
|
||||||
|
|
||||||
|
await config.set(CONFIG_KEYS.SNS_PAGE_CACHE_MAP, map)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取通讯录加载超时阈值(毫秒)
|
// 获取通讯录加载超时阈值(毫秒)
|
||||||
export async function getContactsLoadTimeoutMs(): Promise<number> {
|
export async function getContactsLoadTimeoutMs(): Promise<number> {
|
||||||
const value = await config.get(CONFIG_KEYS.CONTACTS_LOAD_TIMEOUT_MS)
|
const value = await config.get(CONFIG_KEYS.CONTACTS_LOAD_TIMEOUT_MS)
|
||||||
|
|||||||
Reference in New Issue
Block a user