mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat(sns): add my timeline shortcut
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useLayoutEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useEffect, useLayoutEffect, useState, useRef, useCallback, useMemo } from 'react'
|
||||
import { RefreshCw, Search, X, Download, FolderOpen, FileJson, FileText, Image, CheckCircle, AlertCircle, Calendar, Users, Info, ChevronLeft, ChevronRight, Shield, ShieldOff } from 'lucide-react'
|
||||
import JumpToDateDialog from '../components/JumpToDateDialog'
|
||||
import './SnsPage.scss'
|
||||
@@ -21,12 +21,21 @@ interface Contact {
|
||||
username: string
|
||||
displayName: string
|
||||
avatarUrl?: string
|
||||
remark?: string
|
||||
nickname?: string
|
||||
type?: 'friend' | 'former_friend' | 'sns_only'
|
||||
lastSessionTimestamp?: number
|
||||
postCount?: number
|
||||
postCountStatus?: ContactPostCountStatus
|
||||
}
|
||||
|
||||
interface SidebarUserProfile {
|
||||
wxid: string
|
||||
displayName: string
|
||||
alias?: string
|
||||
avatarUrl?: string
|
||||
}
|
||||
|
||||
interface ContactsCountProgress {
|
||||
resolved: number
|
||||
total: number
|
||||
@@ -43,6 +52,38 @@ interface SnsOverviewStats {
|
||||
|
||||
type OverviewStatsStatus = 'loading' | 'ready' | 'error'
|
||||
|
||||
const SIDEBAR_USER_PROFILE_CACHE_KEY = 'sidebar_user_profile_cache_v1'
|
||||
|
||||
const readSidebarUserProfileCache = (): SidebarUserProfile | null => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(SIDEBAR_USER_PROFILE_CACHE_KEY)
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw) as SidebarUserProfile
|
||||
if (!parsed || typeof parsed !== 'object') return null
|
||||
return {
|
||||
wxid: String(parsed.wxid || '').trim(),
|
||||
displayName: String(parsed.displayName || '').trim(),
|
||||
alias: parsed.alias ? String(parsed.alias).trim() : undefined,
|
||||
avatarUrl: parsed.avatarUrl ? String(parsed.avatarUrl).trim() : undefined
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeAccountId = (value?: string | null): string => {
|
||||
const trimmed = String(value || '').trim()
|
||||
if (!trimmed) return ''
|
||||
if (trimmed.toLowerCase().startsWith('wxid_')) {
|
||||
const match = trimmed.match(/^(wxid_[^_]+)/i)
|
||||
return (match?.[1] || trimmed).toLowerCase()
|
||||
}
|
||||
const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/)
|
||||
return (suffixMatch ? suffixMatch[1] : trimmed).toLowerCase()
|
||||
}
|
||||
|
||||
const normalizeNameForCompare = (value?: string | null): string => String(value || '').trim().toLowerCase()
|
||||
|
||||
export default function SnsPage() {
|
||||
const [posts, setPosts] = useState<SnsPost[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -71,6 +112,10 @@ export default function SnsPage() {
|
||||
total: 0,
|
||||
running: false
|
||||
})
|
||||
const [currentUserProfile, setCurrentUserProfile] = useState<SidebarUserProfile>(() => readSidebarUserProfileCache() || {
|
||||
wxid: '',
|
||||
displayName: ''
|
||||
})
|
||||
|
||||
// UI states
|
||||
const [showJumpDialog, setShowJumpDialog] = useState(false)
|
||||
@@ -197,6 +242,61 @@ export default function SnsPage() {
|
||||
return [...input].sort(compareContactsForRanking)
|
||||
}, [compareContactsForRanking])
|
||||
|
||||
const resolvedCurrentUserContact = useMemo(() => {
|
||||
const normalizedWxid = normalizeAccountId(currentUserProfile.wxid)
|
||||
const normalizedAlias = normalizeAccountId(currentUserProfile.alias)
|
||||
const normalizedDisplayName = normalizeNameForCompare(currentUserProfile.displayName)
|
||||
|
||||
if (normalizedWxid) {
|
||||
const exactByUsername = contacts.find((contact) => normalizeAccountId(contact.username) === normalizedWxid)
|
||||
if (exactByUsername) return exactByUsername
|
||||
}
|
||||
|
||||
if (normalizedAlias) {
|
||||
const exactByAliasLikeName = contacts.find((contact) => {
|
||||
const candidates = [contact.displayName, contact.remark, contact.nickname].map(normalizeNameForCompare)
|
||||
return candidates.includes(normalizedAlias)
|
||||
})
|
||||
if (exactByAliasLikeName) return exactByAliasLikeName
|
||||
}
|
||||
|
||||
if (!normalizedDisplayName) return null
|
||||
return contacts.find((contact) => {
|
||||
const candidates = [contact.displayName, contact.remark, contact.nickname].map(normalizeNameForCompare)
|
||||
return candidates.includes(normalizedDisplayName)
|
||||
}) || null
|
||||
}, [contacts, currentUserProfile.alias, currentUserProfile.displayName, currentUserProfile.wxid])
|
||||
|
||||
const currentTimelineTargetContact = useMemo(() => {
|
||||
const normalizedTargetUsername = String(authorTimelineTarget?.username || '').trim()
|
||||
if (!normalizedTargetUsername) return null
|
||||
return contacts.find((contact) => contact.username === normalizedTargetUsername) || null
|
||||
}, [authorTimelineTarget, contacts])
|
||||
|
||||
const myTimelineCount = useMemo(() => {
|
||||
if (typeof overviewStats.myPosts === 'number' && Number.isFinite(overviewStats.myPosts) && overviewStats.myPosts >= 0) {
|
||||
return Math.floor(overviewStats.myPosts)
|
||||
}
|
||||
if (resolvedCurrentUserContact?.postCountStatus === 'ready' && typeof resolvedCurrentUserContact.postCount === 'number') {
|
||||
return normalizePostCount(resolvedCurrentUserContact.postCount)
|
||||
}
|
||||
return null
|
||||
}, [normalizePostCount, overviewStats.myPosts, resolvedCurrentUserContact])
|
||||
|
||||
const myTimelineCountLoading = Boolean(
|
||||
overviewStatsStatus === 'loading'
|
||||
|| resolvedCurrentUserContact?.postCountStatus === 'loading'
|
||||
)
|
||||
|
||||
const openCurrentUserTimeline = useCallback(() => {
|
||||
if (!resolvedCurrentUserContact) return
|
||||
setAuthorTimelineTarget({
|
||||
username: resolvedCurrentUserContact.username,
|
||||
displayName: resolvedCurrentUserContact.displayName || currentUserProfile.displayName || resolvedCurrentUserContact.username,
|
||||
avatarUrl: resolvedCurrentUserContact.avatarUrl || currentUserProfile.avatarUrl
|
||||
})
|
||||
}, [currentUserProfile.avatarUrl, currentUserProfile.displayName, resolvedCurrentUserContact])
|
||||
|
||||
const isDefaultViewNow = useCallback(() => {
|
||||
return selectedUsernamesRef.current.length === 0 && !searchKeywordRef.current.trim() && !jumpTargetDateRef.current
|
||||
}, [])
|
||||
@@ -626,6 +726,8 @@ export default function SnsPage() {
|
||||
username: contact.username,
|
||||
displayName: contact.displayName || contact.username,
|
||||
avatarUrl: cachedAvatarMap[contact.username]?.avatarUrl,
|
||||
remark: contact.remark,
|
||||
nickname: contact.nickname,
|
||||
type: (contact.type === 'former_friend' ? 'former_friend' : 'friend') as 'friend' | 'former_friend',
|
||||
lastSessionTimestamp: 0,
|
||||
postCount: hasCachedCount ? Math.max(0, Math.floor(cachedCount)) : undefined,
|
||||
@@ -677,6 +779,8 @@ export default function SnsPage() {
|
||||
username: c.username,
|
||||
displayName: c.displayName,
|
||||
avatarUrl: c.avatarUrl,
|
||||
remark: c.remark,
|
||||
nickname: c.nickname,
|
||||
type: c.type === 'former_friend' ? 'former_friend' : 'friend',
|
||||
lastSessionTimestamp: Number(sessionTimestampMap.get(c.username) || 0),
|
||||
postCount: hasCachedCount ? Math.max(0, Math.floor(cachedCount)) : undefined,
|
||||
@@ -769,6 +873,39 @@ export default function SnsPage() {
|
||||
loadOverviewStats()
|
||||
}, [hydrateSnsPageCache, loadContacts, loadOverviewStats])
|
||||
|
||||
useEffect(() => {
|
||||
const syncCurrentUserProfile = async () => {
|
||||
const cachedProfile = readSidebarUserProfileCache()
|
||||
if (cachedProfile) {
|
||||
setCurrentUserProfile((prev) => ({
|
||||
wxid: cachedProfile.wxid || prev.wxid,
|
||||
displayName: cachedProfile.displayName || prev.displayName,
|
||||
alias: cachedProfile.alias || prev.alias,
|
||||
avatarUrl: cachedProfile.avatarUrl || prev.avatarUrl
|
||||
}))
|
||||
}
|
||||
|
||||
try {
|
||||
const wxidRaw = await configService.getMyWxid()
|
||||
const resolvedWxid = normalizeAccountId(wxidRaw) || String(wxidRaw || '').trim()
|
||||
if (!resolvedWxid && !cachedProfile) return
|
||||
setCurrentUserProfile((prev) => ({
|
||||
wxid: resolvedWxid || prev.wxid,
|
||||
displayName: prev.displayName || cachedProfile?.displayName || resolvedWxid || '未识别用户',
|
||||
alias: prev.alias || cachedProfile?.alias,
|
||||
avatarUrl: prev.avatarUrl || cachedProfile?.avatarUrl
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to sync current sidebar user profile:', error)
|
||||
}
|
||||
}
|
||||
|
||||
void syncCurrentUserProfile()
|
||||
const handleChange = () => { void syncCurrentUserProfile() }
|
||||
window.addEventListener('wxid-changed', handleChange as EventListener)
|
||||
return () => window.removeEventListener('wxid-changed', handleChange as EventListener)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
contactsCountHydrationTokenRef.current += 1
|
||||
@@ -829,6 +966,24 @@ export default function SnsPage() {
|
||||
<div className="feed-header">
|
||||
<div className="feed-header-main">
|
||||
<h2>朋友圈</h2>
|
||||
<button
|
||||
type="button"
|
||||
className={`feed-my-timeline-entry ${resolvedCurrentUserContact ? 'ready' : ''} ${myTimelineCountLoading ? 'loading' : ''}`}
|
||||
onClick={openCurrentUserTimeline}
|
||||
disabled={!resolvedCurrentUserContact}
|
||||
title={resolvedCurrentUserContact
|
||||
? `打开${resolvedCurrentUserContact.displayName || '我'}的朋友圈详情`
|
||||
: '未在右侧联系人列表中匹配到当前账号'}
|
||||
>
|
||||
<span className="feed-my-timeline-label">我的朋友圈</span>
|
||||
<span className="feed-my-timeline-count">
|
||||
{myTimelineCount !== null
|
||||
? `${myTimelineCount.toLocaleString('zh-CN')} 条`
|
||||
: myTimelineCountLoading
|
||||
? '...'
|
||||
: '--'}
|
||||
</span>
|
||||
</button>
|
||||
<div className={`feed-stats-line ${overviewStatsStatus}`}>
|
||||
{renderOverviewStats()}
|
||||
</div>
|
||||
@@ -985,6 +1140,14 @@ export default function SnsPage() {
|
||||
<ContactSnsTimelineDialog
|
||||
target={authorTimelineTarget}
|
||||
onClose={closeAuthorTimeline}
|
||||
initialTotalPosts={authorTimelineTarget?.username === resolvedCurrentUserContact?.username
|
||||
? myTimelineCount
|
||||
: currentTimelineTargetContact?.postCountStatus === 'ready'
|
||||
? normalizePostCount(currentTimelineTargetContact.postCount)
|
||||
: null}
|
||||
initialTotalPostsLoading={Boolean(authorTimelineTarget?.username === resolvedCurrentUserContact?.username
|
||||
? myTimelineCount === null && myTimelineCountLoading
|
||||
: currentTimelineTargetContact?.postCountStatus === 'loading')}
|
||||
isProtected={triggerInstalled === true}
|
||||
onDeletePost={handlePostDelete}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user