diff --git a/src/components/Sns/ContactSnsTimelineDialog.tsx b/src/components/Sns/ContactSnsTimelineDialog.tsx index a79d9bd..e7e6dde 100644 --- a/src/components/Sns/ContactSnsTimelineDialog.tsx +++ b/src/components/Sns/ContactSnsTimelineDialog.tsx @@ -26,6 +26,8 @@ interface ContactSnsTimelineDialogProps { onClose: () => void initialTotalPosts?: number | null initialTotalPostsLoading?: boolean + isProtected?: boolean + onDeletePost?: (postId: string, username: string) => void } const normalizeTotalPosts = (value?: number | null): number | null => { @@ -90,7 +92,9 @@ export function ContactSnsTimelineDialog({ target, onClose, initialTotalPosts = null, - initialTotalPostsLoading = false + initialTotalPostsLoading = false, + isProtected = false, + onDeletePost }: ContactSnsTimelineDialogProps) { const [timelinePosts, setTimelinePosts] = useState([]) const [timelineLoading, setTimelineLoading] = useState(false) @@ -536,7 +540,7 @@ export function ContactSnsTimelineDialog({ {timelinePosts.map((post) => ( { if (isVideo) { void window.electronAPI.window.openVideoPlayerWindow(src) @@ -545,6 +549,7 @@ export function ContactSnsTimelineDialog({ } }} onDebug={() => {}} + onDelete={onDeletePost} hideAuthorMeta /> ))} diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index f088e76..49abd13 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -5,7 +5,8 @@ import './SnsPage.scss' import { SnsPost } from '../types/sns' import { SnsPostItem } from '../components/Sns/SnsPostItem' import { SnsFilterPanel } from '../components/Sns/SnsFilterPanel' -import { Avatar } from '../components/Avatar' +import { ContactSnsTimelineDialog } from '../components/Sns/ContactSnsTimelineDialog' +import type { ContactSnsTimelineTarget } from '../components/Sns/contactSnsTimeline' import * as configService from '../services/config' const SNS_PAGE_CACHE_TTL_MS = 24 * 60 * 60 * 1000 @@ -42,12 +43,6 @@ interface SnsOverviewStats { type OverviewStatsStatus = 'loading' | 'ready' | 'error' -interface AuthorTimelineTarget { - username: string - nickname: string - avatarUrl?: string -} - export default function SnsPage() { const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(false) @@ -80,13 +75,7 @@ export default function SnsPage() { // UI states const [showJumpDialog, setShowJumpDialog] = useState(false) const [debugPost, setDebugPost] = useState(null) - const [authorTimelineTarget, setAuthorTimelineTarget] = useState(null) - const [authorTimelinePosts, setAuthorTimelinePosts] = useState([]) - const [authorTimelineLoading, setAuthorTimelineLoading] = useState(false) - const [authorTimelineLoadingMore, setAuthorTimelineLoadingMore] = useState(false) - const [authorTimelineHasMore, setAuthorTimelineHasMore] = useState(false) - const [authorTimelineTotalPosts, setAuthorTimelineTotalPosts] = useState(null) - const [authorTimelineStatsLoading, setAuthorTimelineStatsLoading] = useState(false) + const [authorTimelineTarget, setAuthorTimelineTarget] = useState(null) // 导出相关状态 const [showExportDialog, setShowExportDialog] = useState(false) @@ -125,10 +114,6 @@ export default function SnsPage() { const contactsLoadTokenRef = useRef(0) const contactsCountHydrationTokenRef = useRef(0) const contactsCountBatchTimerRef = useRef(null) - const authorTimelinePostsRef = useRef([]) - const authorTimelineLoadingRef = useRef(false) - const authorTimelineRequestTokenRef = useRef(0) - const authorTimelineStatsTokenRef = useRef(0) // Sync posts ref useEffect(() => { @@ -152,9 +137,6 @@ export default function SnsPage() { useEffect(() => { jumpTargetDateRef.current = jumpTargetDate }, [jumpTargetDate]) - useEffect(() => { - authorTimelinePostsRef.current = authorTimelinePosts - }, [authorTimelinePosts]) // 在 DOM 更新后、浏览器绘制前同步调整滚动位置,防止向上加载时页面跳动 useLayoutEffect(() => { const snapshot = scrollAdjustmentRef.current; @@ -760,137 +742,16 @@ export default function SnsPage() { }, [ensureSnsUserPostCountsCacheScopeKey, hydrateContactPostCounts, sortContactsForRanking, stopContactsCountHydration]) const closeAuthorTimeline = useCallback(() => { - authorTimelineRequestTokenRef.current += 1 - authorTimelineStatsTokenRef.current += 1 - authorTimelineLoadingRef.current = false setAuthorTimelineTarget(null) - setAuthorTimelinePosts([]) - setAuthorTimelineLoading(false) - setAuthorTimelineLoadingMore(false) - setAuthorTimelineHasMore(false) - setAuthorTimelineTotalPosts(null) - setAuthorTimelineStatsLoading(false) - }, []) - - const loadAuthorTimelineTotalPosts = useCallback(async (target: AuthorTimelineTarget) => { - const requestToken = ++authorTimelineStatsTokenRef.current - setAuthorTimelineStatsLoading(true) - setAuthorTimelineTotalPosts(null) - - try { - const result = await window.electronAPI.sns.getUserPostCounts() - if (requestToken !== authorTimelineStatsTokenRef.current) return - - if (result.success && result.counts) { - const totalPosts = result.counts[target.username] ?? 0 - setAuthorTimelineTotalPosts(Math.max(0, Number(totalPosts || 0))) - } else { - setAuthorTimelineTotalPosts(null) - } - } catch (error) { - console.error('Failed to load author timeline total posts:', error) - if (requestToken === authorTimelineStatsTokenRef.current) { - setAuthorTimelineTotalPosts(null) - } - } finally { - if (requestToken === authorTimelineStatsTokenRef.current) { - setAuthorTimelineStatsLoading(false) - } - } - }, []) - - const loadAuthorTimelinePosts = useCallback(async (target: AuthorTimelineTarget, options: { reset?: boolean } = {}) => { - const { reset = false } = options - if (authorTimelineLoadingRef.current) return - - authorTimelineLoadingRef.current = true - if (reset) { - setAuthorTimelineLoading(true) - setAuthorTimelineLoadingMore(false) - setAuthorTimelineHasMore(false) - } else { - setAuthorTimelineLoadingMore(true) - } - - const requestToken = ++authorTimelineRequestTokenRef.current - - try { - const limit = 20 - let endTs: number | undefined = undefined - - if (!reset && authorTimelinePostsRef.current.length > 0) { - endTs = authorTimelinePostsRef.current[authorTimelinePostsRef.current.length - 1].createTime - 1 - } - - const result = await window.electronAPI.sns.getTimeline( - limit, - 0, - [target.username], - '', - undefined, - endTs - ) - - if (requestToken !== authorTimelineRequestTokenRef.current) return - if (!result.success || !result.timeline) { - if (reset) { - setAuthorTimelinePosts([]) - setAuthorTimelineHasMore(false) - } - return - } - - if (reset) { - const sorted = [...result.timeline].sort((a, b) => b.createTime - a.createTime) - setAuthorTimelinePosts(sorted) - setAuthorTimelineHasMore(result.timeline.length >= limit) - return - } - - const existingIds = new Set(authorTimelinePostsRef.current.map((p) => p.id)) - const uniqueOlder = result.timeline.filter((p) => !existingIds.has(p.id)) - if (uniqueOlder.length > 0) { - const merged = [...authorTimelinePostsRef.current, ...uniqueOlder].sort((a, b) => b.createTime - a.createTime) - setAuthorTimelinePosts(merged) - } - if (result.timeline.length < limit) { - setAuthorTimelineHasMore(false) - } - } catch (error) { - console.error('Failed to load author timeline:', error) - if (requestToken === authorTimelineRequestTokenRef.current && reset) { - setAuthorTimelinePosts([]) - setAuthorTimelineHasMore(false) - } - } finally { - if (requestToken === authorTimelineRequestTokenRef.current) { - authorTimelineLoadingRef.current = false - setAuthorTimelineLoading(false) - setAuthorTimelineLoadingMore(false) - } - } }, []) const openAuthorTimeline = useCallback((post: SnsPost) => { - authorTimelineRequestTokenRef.current += 1 - authorTimelineLoadingRef.current = false - const target = { + setAuthorTimelineTarget({ username: post.username, - nickname: post.nickname, + displayName: decodeHtmlEntities(post.nickname || '') || post.username, avatarUrl: post.avatarUrl - } - setAuthorTimelineTarget(target) - setAuthorTimelinePosts([]) - setAuthorTimelineHasMore(false) - setAuthorTimelineTotalPosts(null) - void loadAuthorTimelinePosts(target, { reset: true }) - void loadAuthorTimelineTotalPosts(target) - }, [loadAuthorTimelinePosts, loadAuthorTimelineTotalPosts]) - - const loadMoreAuthorTimeline = useCallback(() => { - if (!authorTimelineTarget || authorTimelineLoading || authorTimelineLoadingMore || !authorTimelineHasMore) return - void loadAuthorTimelinePosts(authorTimelineTarget, { reset: false }) - }, [authorTimelineHasMore, authorTimelineLoading, authorTimelineLoadingMore, authorTimelineTarget, loadAuthorTimelinePosts]) + }) + }, [decodeHtmlEntities]) const handlePostDelete = useCallback((postId: string, username: string) => { setPosts(prev => { @@ -898,12 +759,8 @@ export default function SnsPage() { void persistSnsPageCache({ posts: next }) return next }) - setAuthorTimelinePosts(prev => prev.filter(p => p.id !== postId)) - if (authorTimelineTarget && authorTimelineTarget.username === username) { - setAuthorTimelineTotalPosts(prev => prev === null ? null : Math.max(0, prev - 1)) - } void loadOverviewStats() - }, [authorTimelineTarget, loadOverviewStats, persistSnsPageCache]) + }, [loadOverviewStats, persistSnsPageCache]) // Initial Load & Listeners useEffect(() => { @@ -947,24 +804,6 @@ export default function SnsPage() { return () => clearTimeout(timer) }, [selectedUsernames, searchKeyword, jumpTargetDate, loadPosts]) - useEffect(() => { - if (!authorTimelineTarget) return - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - closeAuthorTimeline() - } - } - window.addEventListener('keydown', handleKeyDown) - return () => window.removeEventListener('keydown', handleKeyDown) - }, [authorTimelineTarget, closeAuthorTimeline]) - - useEffect(() => { - if (authorTimelineTotalPosts === null) return - if (authorTimelinePosts.length >= authorTimelineTotalPosts) { - setAuthorTimelineHasMore(false) - } - }, [authorTimelinePosts.length, authorTimelineTotalPosts]) - const handleScroll = (e: React.UIEvent) => { const { scrollTop, clientHeight, scrollHeight } = e.currentTarget if (scrollHeight - scrollTop - clientHeight < 400 && hasMore && !loading && !loadingNewer) { @@ -983,29 +822,6 @@ export default function SnsPage() { } } - const handleAuthorTimelineScroll = (e: React.UIEvent) => { - const { scrollTop, clientHeight, scrollHeight } = e.currentTarget - if (scrollHeight - scrollTop - clientHeight < 260) { - loadMoreAuthorTimeline() - } - } - - const renderAuthorTimelineStats = () => { - const loadedCount = authorTimelinePosts.length - const loadPart = authorTimelineStatsLoading - ? `已加载 ${loadedCount} / 总数统计中...` - : authorTimelineTotalPosts === null - ? `已加载 ${loadedCount} 条` - : `已加载 ${loadedCount} / 共 ${authorTimelineTotalPosts} 条` - - if (authorTimelineLoading && loadedCount === 0) return `${loadPart} | 加载中...` - if (loadedCount === 0) return loadPart - - const latest = authorTimelinePosts[0]?.createTime ?? null - const earliest = authorTimelinePosts[authorTimelinePosts.length - 1]?.createTime ?? null - return `${loadPart} | ${formatDateOnly(earliest)} ~ ${formatDateOnly(latest)}` - } - return (
@@ -1166,76 +982,12 @@ export default function SnsPage() { currentDate={jumpTargetDate || new Date()} /> - {authorTimelineTarget && ( -
-
e.stopPropagation()}> -
-
- -
-

{decodeHtmlEntities(authorTimelineTarget.nickname)}

-
@{authorTimelineTarget.username}
-
{renderAuthorTimelineStats()}
-
-
- -
- -
- {authorTimelinePosts.length > 0 && ( -
- {authorTimelinePosts.map(post => ( - { - if (isVideo) { - void window.electronAPI.window.openVideoPlayerWindow(src) - } else { - void window.electronAPI.window.openImageViewerWindow(src, liveVideoPath || undefined) - } - }} - onDebug={(p) => setDebugPost(p)} - onDelete={handlePostDelete} - onOpenAuthorPosts={openAuthorTimeline} - hideAuthorMeta - /> - ))} -
- )} - - {authorTimelineLoading && ( -
- - 正在加载该用户朋友圈... -
- )} - - {!authorTimelineLoading && authorTimelinePosts.length === 0 && ( -
该用户暂无朋友圈
- )} - - {!authorTimelineLoading && authorTimelineHasMore && ( - - )} -
-
-
- )} + {debugPost && (
setDebugPost(null)}>