import { useEffect, useState, useRef, useCallback, useMemo } from 'react' import { RefreshCw, Heart, Search, Calendar, User, X, Filter, Play, ImageIcon } from 'lucide-react' import { Avatar } from '../components/Avatar' import { ImagePreview } from '../components/ImagePreview' import JumpToDateDialog from '../components/JumpToDateDialog' import './SnsPage.scss' interface SnsPost { id: string username: string nickname: string avatarUrl?: string createTime: number contentDesc: string type?: number media: { url: string; thumb: string }[] likes: string[] comments: { id: string; nickname: string; content: string; refCommentId: string; refNickname?: string }[] } const MediaItem = ({ url, thumb, onPreview }: { url: string, thumb: string, onPreview: () => void }) => { const [error, setError] = useState(false); return (
{!error ? ( setError(true)} /> ) : (
)}
); }; interface Contact { username: string displayName: string avatarUrl?: string } export default function SnsPage() { const [posts, setPosts] = useState([]) const [loading, setLoading] = useState(false) const [offset, setOffset] = useState(0) const [hasMore, setHasMore] = useState(true) const loadingRef = useRef(false) // 筛选与搜索状态 const [searchKeyword, setSearchKeyword] = useState('') const [selectedUsernames, setSelectedUsernames] = useState([]) const [isSidebarOpen, setIsSidebarOpen] = useState(true) // 联系人列表状态 const [contacts, setContacts] = useState([]) const [contactSearch, setContactSearch] = useState('') const [contactsLoading, setContactsLoading] = useState(false) const [showJumpDialog, setShowJumpDialog] = useState(false) const [jumpTargetDate, setJumpTargetDate] = useState(undefined) const [previewImage, setPreviewImage] = useState(null) const postsContainerRef = useRef(null) const [hasNewer, setHasNewer] = useState(false) const [loadingNewer, setLoadingNewer] = useState(false) const postsRef = useRef([]) const scrollAdjustmentRef = useRef(0) // 同步 posts 到 ref 供 loadPosts 使用 useEffect(() => { postsRef.current = posts }, [posts]) // 处理向上加载动态时的滚动位置保持 useEffect(() => { if (scrollAdjustmentRef.current !== 0 && postsContainerRef.current) { const container = postsContainerRef.current; const newHeight = container.scrollHeight; const diff = newHeight - scrollAdjustmentRef.current; if (diff > 0) { container.scrollTop += diff; } scrollAdjustmentRef.current = 0; } }, [posts]) const loadPosts = useCallback(async (options: { reset?: boolean, direction?: 'older' | 'newer' } = {}) => { const { reset = false, direction = 'older' } = options if (loadingRef.current) return loadingRef.current = true if (direction === 'newer') setLoadingNewer(true) else setLoading(true) try { const limit = 20 let startTs: number | undefined = undefined let endTs: number | undefined = undefined if (reset) { if (jumpTargetDate) { endTs = Math.floor(jumpTargetDate.getTime() / 1000) + 86399 } } else if (direction === 'newer') { const currentPosts = postsRef.current if (currentPosts.length > 0) { const topTs = currentPosts[0].createTime console.log('[SnsPage] Fetching newer posts starts from:', topTs + 1); const result = await window.electronAPI.sns.getTimeline( limit, 0, selectedUsernames, searchKeyword, topTs + 1, undefined ); if (result.success && result.timeline && result.timeline.length > 0) { if (postsContainerRef.current) { scrollAdjustmentRef.current = postsContainerRef.current.scrollHeight; } const existingIds = new Set(currentPosts.map(p => p.id)); const uniqueNewer = result.timeline.filter(p => !existingIds.has(p.id)); if (uniqueNewer.length > 0) { setPosts(prev => [...uniqueNewer, ...prev]); } setHasNewer(result.timeline.length >= limit); } else { setHasNewer(false); } } setLoadingNewer(false); loadingRef.current = false; return; } else { const currentPosts = postsRef.current if (currentPosts.length > 0) { endTs = currentPosts[currentPosts.length - 1].createTime - 1 } } const result = await window.electronAPI.sns.getTimeline( limit, 0, selectedUsernames, searchKeyword, startTs, endTs ) if (result.success && result.timeline) { if (reset) { setPosts(result.timeline) setHasMore(result.timeline.length >= limit) // 探测上方是否还有新动态(利用 DLL 过滤,而非底层 SQL) const topTs = result.timeline[0]?.createTime || 0; if (topTs > 0) { const checkResult = await window.electronAPI.sns.getTimeline(1, 0, selectedUsernames, searchKeyword, topTs + 1, undefined); setHasNewer(!!(checkResult.success && checkResult.timeline && checkResult.timeline.length > 0)); } else { setHasNewer(false); } if (postsContainerRef.current) { postsContainerRef.current.scrollTop = 0 } } else { if (result.timeline.length > 0) { setPosts(prev => [...prev, ...result.timeline!]) } if (result.timeline.length < limit) { setHasMore(false) } } } } catch (error) { console.error('Failed to load SNS timeline:', error) } finally { setLoading(false) setLoadingNewer(false) loadingRef.current = false } }, [selectedUsernames, searchKeyword, jumpTargetDate]) // 获取联系人列表 const loadContacts = useCallback(async () => { setContactsLoading(true) try { const result = await window.electronAPI.chat.getSessions() if (result.success && result.sessions) { const systemAccounts = ['filehelper', 'fmessage', 'newsapp', 'weixin', 'qqmail', 'tmessage', 'floatbottle', 'medianote', 'brandsessionholder']; const initialContacts = result.sessions .filter((s: any) => { if (!s.username) return false; const u = s.username.toLowerCase(); if (u.includes('@chatroom') || u.endsWith('@chatroom') || u.endsWith('@openim')) return false; if (u.startsWith('gh_')) return false; if (systemAccounts.includes(u) || u.includes('helper') || u.includes('sessionholder')) return false; return true; }) .map((s: any) => ({ username: s.username, displayName: s.displayName || s.username, avatarUrl: s.avatarUrl })) setContacts(initialContacts) const usernames = initialContacts.map(c => c.username) const enriched = await window.electronAPI.chat.enrichSessionsContactInfo(usernames) if (enriched.success && enriched.contacts) { setContacts(prev => prev.map(c => { const extra = enriched.contacts![c.username] if (extra) { return { ...c, displayName: extra.displayName || c.displayName, avatarUrl: extra.avatarUrl || c.avatarUrl } } return c })) } } } catch (error) { console.error('Failed to load contacts:', error) } finally { setContactsLoading(false) } }, []) // 初始加载 useEffect(() => { const checkSchema = async () => { try { const schema = await window.electronAPI.chat.execQuery('sns', null, "PRAGMA table_info(SnsTimeLine)"); console.log('[SnsPage] SnsTimeLine Schema:', schema); if (schema.success && schema.rows) { const columns = schema.rows.map((r: any) => r.name); console.log('[SnsPage] Available columns:', columns); } } catch (e) { console.error('[SnsPage] Failed to check schema:', e); } }; checkSchema(); loadContacts() }, [loadContacts]) useEffect(() => { const handleChange = () => { setPosts([]) setHasMore(true) setHasNewer(false) setSelectedUsernames([]) setSearchKeyword('') setJumpTargetDate(null) loadContacts() loadPosts({ reset: true }) } window.addEventListener('wxid-changed', handleChange as EventListener) return () => window.removeEventListener('wxid-changed', handleChange as EventListener) }, [loadContacts, loadPosts]) useEffect(() => { loadPosts({ reset: true }) }, [selectedUsernames, searchKeyword, jumpTargetDate]) const handleScroll = (e: React.UIEvent) => { const { scrollTop, clientHeight, scrollHeight } = e.currentTarget // 加载更旧的动态(触底) if (scrollHeight - scrollTop - clientHeight < 400 && hasMore && !loading && !loadingNewer) { loadPosts({ direction: 'older' }) } // 加载更新的动态(触顶触发) // 这里的阈值可以保留,但主要依赖下面的 handleWheel 捕获到顶后的上划 if (scrollTop < 10 && hasNewer && !loading && !loadingNewer) { loadPosts({ direction: 'newer' }) } } // 处理到顶后的手动上滚意图 const handleWheel = (e: React.WheelEvent) => { const container = postsContainerRef.current if (!container) return // deltaY < 0 表示向上滚,scrollTop === 0 表示已经在最顶端 if (e.deltaY < -20 && container.scrollTop <= 0 && hasNewer && !loading && !loadingNewer) { console.log('[SnsPage] Wheel-up detected at top, loading newer posts...'); loadPosts({ direction: 'newer' }) } } const formatTime = (ts: number) => { const date = new Date(ts * 1000) const isCurrentYear = date.getFullYear() === new Date().getFullYear() return date.toLocaleString('zh-CN', { year: isCurrentYear ? undefined : 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }) } const toggleUserSelection = (username: string) => { // 选择联系人时,如果当前有时间跳转,建议清除时间跳转以避免“跳到旧动态”的困惑 // 或者保持原样。根据用户反馈“乱跳”,我们在这里选择: // 如果用户选择了新的一个人,而之前有时间跳转,我们重置时间跳转到最新。 setJumpTargetDate(undefined); setSelectedUsernames(prev => { if (prev.includes(username)) { return prev.filter(u => u !== username) } else { return [...prev, username] } }) } const clearFilters = () => { setSearchKeyword('') setSelectedUsernames([]) setJumpTargetDate(undefined) } const filteredContacts = contacts.filter(c => c.displayName.toLowerCase().includes(contactSearch.toLowerCase()) || c.username.toLowerCase().includes(contactSearch.toLowerCase()) ) return (
{/* 侧边栏:过滤与搜索 */}
{!isSidebarOpen && ( )}

社交动态

{loadingNewer && (
正在检查更新的动态...
)} {!loadingNewer && hasNewer && (
loadPosts({ direction: 'newer' })}> 查看更新的动态
)} {posts.map((post, index) => { return (
{post.nickname}
{formatTime(post.createTime)}
{post.contentDesc &&
{post.contentDesc}
} {post.type === 15 ? (
视频动态
) : post.media.length > 0 && (
{post.media.map((m, idx) => ( setPreviewImage(m.url)} /> ))}
)}
{(post.likes.length > 0 || post.comments.length > 0) && (
{post.likes.length > 0 && (
{post.likes.join('、')}
)} {post.comments.length > 0 && (
{post.comments.map((c, idx) => (
{c.nickname} {c.refNickname && ( <> 回复 {c.refNickname} )} : {c.content}
))}
)}
)}
) })}
{loading &&
正在加载更多...
} {!hasMore && posts.length > 0 &&
已经到底啦
} {!loading && posts.length === 0 && (

未找到相关动态

{(selectedUsernames.length > 0 || searchKeyword) && ( )}
)}
{previewImage && ( setPreviewImage(null)} /> )} { setShowJumpDialog(false) }} onSelect={(date) => { setJumpTargetDate(date) setShowJumpDialog(false) }} currentDate={jumpTargetDate || new Date()} />
) }