mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
refactor(sns): open contact timeline from sidebar
This commit is contained in:
@@ -20,43 +20,33 @@ interface SnsFilterPanelProps {
|
|||||||
searchKeyword: string
|
searchKeyword: string
|
||||||
setSearchKeyword: (val: string) => void
|
setSearchKeyword: (val: string) => void
|
||||||
totalFriendsLabel?: string
|
totalFriendsLabel?: string
|
||||||
selectedUsernames: string[]
|
|
||||||
setSelectedUsernames: (val: string[]) => void
|
|
||||||
contacts: Contact[]
|
contacts: Contact[]
|
||||||
contactSearch: string
|
contactSearch: string
|
||||||
setContactSearch: (val: string) => void
|
setContactSearch: (val: string) => void
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
contactsCountProgress?: ContactsCountProgress
|
contactsCountProgress?: ContactsCountProgress
|
||||||
|
onOpenContactTimeline: (contact: Contact) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
||||||
searchKeyword,
|
searchKeyword,
|
||||||
setSearchKeyword,
|
setSearchKeyword,
|
||||||
totalFriendsLabel,
|
totalFriendsLabel,
|
||||||
selectedUsernames,
|
|
||||||
setSelectedUsernames,
|
|
||||||
contacts,
|
contacts,
|
||||||
contactSearch,
|
contactSearch,
|
||||||
setContactSearch,
|
setContactSearch,
|
||||||
loading,
|
loading,
|
||||||
contactsCountProgress
|
contactsCountProgress,
|
||||||
|
onOpenContactTimeline
|
||||||
}) => {
|
}) => {
|
||||||
const filteredContacts = contacts.filter(c =>
|
const filteredContacts = contacts.filter(c =>
|
||||||
(c.displayName || '').toLowerCase().includes(contactSearch.toLowerCase()) ||
|
(c.displayName || '').toLowerCase().includes(contactSearch.toLowerCase()) ||
|
||||||
c.username.toLowerCase().includes(contactSearch.toLowerCase())
|
c.username.toLowerCase().includes(contactSearch.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggleUserSelection = (username: string) => {
|
|
||||||
if (selectedUsernames.includes(username)) {
|
|
||||||
setSelectedUsernames(selectedUsernames.filter(u => u !== username))
|
|
||||||
} else {
|
|
||||||
setSelectedUsernames([...selectedUsernames, username])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
setSearchKeyword('')
|
setSearchKeyword('')
|
||||||
setSelectedUsernames([])
|
setContactSearch('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEmptyStateText = () => {
|
const getEmptyStateText = () => {
|
||||||
@@ -73,7 +63,7 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
<aside className="sns-filter-panel">
|
<aside className="sns-filter-panel">
|
||||||
<div className="filter-header">
|
<div className="filter-header">
|
||||||
<h3>筛选条件</h3>
|
<h3>筛选条件</h3>
|
||||||
{(searchKeyword || selectedUsernames.length > 0) && (
|
{(searchKeyword || contactSearch) && (
|
||||||
<button className="reset-all-btn" onClick={clearFilters} title="重置所有筛选">
|
<button className="reset-all-btn" onClick={clearFilters} title="重置所有筛选">
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
</button>
|
</button>
|
||||||
@@ -106,9 +96,6 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
<div className="widget-header">
|
<div className="widget-header">
|
||||||
<User size={14} />
|
<User size={14} />
|
||||||
<span>联系人</span>
|
<span>联系人</span>
|
||||||
{selectedUsernames.length > 0 && (
|
|
||||||
<span className="badge">{selectedUsernames.length}</span>
|
|
||||||
)}
|
|
||||||
{totalFriendsLabel && (
|
{totalFriendsLabel && (
|
||||||
<span className="widget-header-summary">{totalFriendsLabel}</span>
|
<span className="widget-header-summary">{totalFriendsLabel}</span>
|
||||||
)}
|
)}
|
||||||
@@ -141,8 +128,8 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={contact.username}
|
key={contact.username}
|
||||||
className={`contact-row ${selectedUsernames.includes(contact.username) ? 'selected' : ''}`}
|
className="contact-row"
|
||||||
onClick={() => toggleUserSelection(contact.username)}
|
onClick={() => onOpenContactTimeline(contact)}
|
||||||
>
|
>
|
||||||
<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">
|
||||||
|
|||||||
@@ -1228,9 +1228,8 @@
|
|||||||
border-radius: var(--sns-border-radius-md);
|
border-radius: var(--sns-border-radius-md);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s ease, transform 0.2s ease;
|
transition: background 0.2s ease, transform 0.2s ease;
|
||||||
border: 2px solid transparent;
|
border: 1px solid transparent;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
/* Separation for unselected items */
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--hover-bg);
|
background: var(--hover-bg);
|
||||||
@@ -1238,41 +1237,6 @@
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background: rgba(var(--primary-rgb), 0.1);
|
|
||||||
border-color: var(--primary);
|
|
||||||
box-shadow: none;
|
|
||||||
z-index: 5;
|
|
||||||
margin-bottom: 0;
|
|
||||||
/* Remove margin to merge */
|
|
||||||
|
|
||||||
.contact-meta {
|
|
||||||
.contact-name {
|
|
||||||
color: var(--primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* If the NEXT item is also selected */
|
|
||||||
&:has(+ .contact-row.selected) {
|
|
||||||
border-bottom: none;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
/* Compensate for missing border (+2px) */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* If the PREVIOUS item is selected */
|
|
||||||
&.selected+.contact-row.selected {
|
|
||||||
border-top: none;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
padding-top: 12px;
|
|
||||||
/* Compensate for missing border */
|
|
||||||
}
|
|
||||||
|
|
||||||
.contact-meta {
|
.contact-meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1315,13 +1279,6 @@
|
|||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
|
||||||
.contact-post-count {
|
|
||||||
color: var(--primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useLayoutEffect, useState, useRef, useCallback, useMemo } 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, Loader2 } from 'lucide-react'
|
import { RefreshCw, Search, X, Download, FolderOpen, FileJson, FileText, Image, CheckCircle, AlertCircle, Calendar, Info, ChevronLeft, ChevronRight, Shield, ShieldOff, Loader2 } from 'lucide-react'
|
||||||
import './SnsPage.scss'
|
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'
|
||||||
@@ -100,7 +100,6 @@ export default function SnsPage() {
|
|||||||
|
|
||||||
// Filter states
|
// Filter states
|
||||||
const [searchKeyword, setSearchKeyword] = useState('')
|
const [searchKeyword, setSearchKeyword] = useState('')
|
||||||
const [selectedUsernames, setSelectedUsernames] = useState<string[]>([])
|
|
||||||
const [jumpTargetDate, setJumpTargetDate] = useState<Date | undefined>(undefined)
|
const [jumpTargetDate, setJumpTargetDate] = useState<Date | undefined>(undefined)
|
||||||
|
|
||||||
// Contacts state
|
// Contacts state
|
||||||
@@ -156,7 +155,6 @@ export default function SnsPage() {
|
|||||||
const contactsRef = useRef<Contact[]>([])
|
const contactsRef = useRef<Contact[]>([])
|
||||||
const overviewStatsRef = useRef<SnsOverviewStats>(overviewStats)
|
const overviewStatsRef = useRef<SnsOverviewStats>(overviewStats)
|
||||||
const overviewStatsStatusRef = useRef<OverviewStatsStatus>(overviewStatsStatus)
|
const overviewStatsStatusRef = useRef<OverviewStatsStatus>(overviewStatsStatus)
|
||||||
const selectedUsernamesRef = useRef<string[]>(selectedUsernames)
|
|
||||||
const searchKeywordRef = useRef(searchKeyword)
|
const searchKeywordRef = useRef(searchKeyword)
|
||||||
const jumpTargetDateRef = useRef<Date | undefined>(jumpTargetDate)
|
const jumpTargetDateRef = useRef<Date | undefined>(jumpTargetDate)
|
||||||
const cacheScopeKeyRef = useRef('')
|
const cacheScopeKeyRef = useRef('')
|
||||||
@@ -181,9 +179,6 @@ export default function SnsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
overviewStatsStatusRef.current = overviewStatsStatus
|
overviewStatsStatusRef.current = overviewStatsStatus
|
||||||
}, [overviewStatsStatus])
|
}, [overviewStatsStatus])
|
||||||
useEffect(() => {
|
|
||||||
selectedUsernamesRef.current = selectedUsernames
|
|
||||||
}, [selectedUsernames])
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
searchKeywordRef.current = searchKeyword
|
searchKeywordRef.current = searchKeyword
|
||||||
}, [searchKeyword])
|
}, [searchKeyword])
|
||||||
@@ -391,7 +386,7 @@ export default function SnsPage() {
|
|||||||
}, [currentUserProfile.avatarUrl, currentUserProfile.displayName, resolvedCurrentUserContact])
|
}, [currentUserProfile.avatarUrl, currentUserProfile.displayName, resolvedCurrentUserContact])
|
||||||
|
|
||||||
const isDefaultViewNow = useCallback(() => {
|
const isDefaultViewNow = useCallback(() => {
|
||||||
return selectedUsernamesRef.current.length === 0 && !searchKeywordRef.current.trim() && !jumpTargetDateRef.current
|
return !searchKeywordRef.current.trim() && !jumpTargetDateRef.current
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const ensureSnsCacheScopeKey = useCallback(async () => {
|
const ensureSnsCacheScopeKey = useCallback(async () => {
|
||||||
@@ -577,7 +572,7 @@ export default function SnsPage() {
|
|||||||
const result = await window.electronAPI.sns.getTimeline(
|
const result = await window.electronAPI.sns.getTimeline(
|
||||||
limit,
|
limit,
|
||||||
0,
|
0,
|
||||||
selectedUsernames,
|
undefined,
|
||||||
searchKeyword,
|
searchKeyword,
|
||||||
topTs + 1,
|
topTs + 1,
|
||||||
undefined
|
undefined
|
||||||
@@ -618,7 +613,7 @@ export default function SnsPage() {
|
|||||||
const result = await window.electronAPI.sns.getTimeline(
|
const result = await window.electronAPI.sns.getTimeline(
|
||||||
limit,
|
limit,
|
||||||
0,
|
0,
|
||||||
selectedUsernames,
|
undefined,
|
||||||
searchKeyword,
|
searchKeyword,
|
||||||
startTs, // default undefined
|
startTs, // default undefined
|
||||||
endTs
|
endTs
|
||||||
@@ -633,7 +628,7 @@ export default function SnsPage() {
|
|||||||
// Check for newer items above topTs
|
// Check for newer items above topTs
|
||||||
const topTs = result.timeline[0]?.createTime || 0;
|
const topTs = result.timeline[0]?.createTime || 0;
|
||||||
if (topTs > 0) {
|
if (topTs > 0) {
|
||||||
const checkResult = await window.electronAPI.sns.getTimeline(1, 0, selectedUsernames, searchKeyword, topTs + 1, undefined);
|
const checkResult = await window.electronAPI.sns.getTimeline(1, 0, undefined, searchKeyword, topTs + 1, undefined);
|
||||||
setHasNewer(!!(checkResult.success && checkResult.timeline && checkResult.timeline.length > 0));
|
setHasNewer(!!(checkResult.success && checkResult.timeline && checkResult.timeline.length > 0));
|
||||||
} else {
|
} else {
|
||||||
setHasNewer(false);
|
setHasNewer(false);
|
||||||
@@ -660,7 +655,7 @@ export default function SnsPage() {
|
|||||||
setLoadingNewer(false)
|
setLoadingNewer(false)
|
||||||
loadingRef.current = false
|
loadingRef.current = false
|
||||||
}
|
}
|
||||||
}, [jumpTargetDate, persistSnsPageCache, searchKeyword, selectedUsernames])
|
}, [jumpTargetDate, persistSnsPageCache, searchKeyword])
|
||||||
|
|
||||||
const stopContactsCountHydration = useCallback((resetProgress = false) => {
|
const stopContactsCountHydration = useCallback((resetProgress = false) => {
|
||||||
contactsCountHydrationTokenRef.current += 1
|
contactsCountHydrationTokenRef.current += 1
|
||||||
@@ -950,6 +945,14 @@ export default function SnsPage() {
|
|||||||
})
|
})
|
||||||
}, [decodeHtmlEntities])
|
}, [decodeHtmlEntities])
|
||||||
|
|
||||||
|
const openContactTimeline = useCallback((contact: Contact) => {
|
||||||
|
setAuthorTimelineTarget({
|
||||||
|
username: contact.username,
|
||||||
|
displayName: contact.displayName || contact.username,
|
||||||
|
avatarUrl: contact.avatarUrl
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handlePostDelete = useCallback((postId: string, username: string) => {
|
const handlePostDelete = useCallback((postId: string, username: string) => {
|
||||||
setPosts(prev => {
|
setPosts(prev => {
|
||||||
const next = prev.filter(p => p.id !== postId)
|
const next = prev.filter(p => p.id !== postId)
|
||||||
@@ -1017,7 +1020,7 @@ export default function SnsPage() {
|
|||||||
stopContactsCountHydration(true)
|
stopContactsCountHydration(true)
|
||||||
setContacts([])
|
setContacts([])
|
||||||
setPosts([]); setHasMore(true); setHasNewer(false);
|
setPosts([]); setHasMore(true); setHasNewer(false);
|
||||||
setSelectedUsernames([]); setSearchKeyword(''); setJumpTargetDate(undefined);
|
setSearchKeyword(''); setJumpTargetDate(undefined);
|
||||||
void hydrateSnsPageCache()
|
void hydrateSnsPageCache()
|
||||||
loadContacts();
|
loadContacts();
|
||||||
loadOverviewStats();
|
loadOverviewStats();
|
||||||
@@ -1032,7 +1035,7 @@ export default function SnsPage() {
|
|||||||
loadPosts({ reset: true })
|
loadPosts({ reset: true })
|
||||||
}, 500)
|
}, 500)
|
||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [selectedUsernames, searchKeyword, jumpTargetDate, loadPosts])
|
}, [searchKeyword, jumpTargetDate, loadPosts])
|
||||||
|
|
||||||
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
const { scrollTop, clientHeight, scrollHeight } = e.currentTarget
|
const { scrollTop, clientHeight, scrollHeight } = e.currentTarget
|
||||||
@@ -1251,21 +1254,16 @@ export default function SnsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!hasMore && posts.length > 0 && (
|
{!hasMore && posts.length > 0 && (
|
||||||
<div className="status-indicator no-more">{
|
<div className="status-indicator no-more">或许过往已无可溯洄,但好在还有可以与你相遇的明天</div>
|
||||||
selectedUsernames.length === 1 &&
|
|
||||||
contacts.find(c => c.username === selectedUsernames[0])?.type === 'former_friend'
|
|
||||||
? '在时间的长河里刻舟求剑'
|
|
||||||
: '或许过往已无可溯洄,但好在还有可以与你相遇的明天'
|
|
||||||
}</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && posts.length === 0 && (
|
{!loading && posts.length === 0 && (
|
||||||
<div className="no-results">
|
<div className="no-results">
|
||||||
<div className="no-results-icon"><Search size={48} /></div>
|
<div className="no-results-icon"><Search size={48} /></div>
|
||||||
<p>未找到相关动态</p>
|
<p>未找到相关动态</p>
|
||||||
{(selectedUsernames.length > 0 || searchKeyword || jumpTargetDate) && (
|
{(searchKeyword || jumpTargetDate) && (
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
setSearchKeyword(''); setSelectedUsernames([]); setJumpTargetDate(undefined);
|
setSearchKeyword(''); setJumpTargetDate(undefined);
|
||||||
}} className="reset-inline">
|
}} className="reset-inline">
|
||||||
重置筛选条件
|
重置筛选条件
|
||||||
</button>
|
</button>
|
||||||
@@ -1286,13 +1284,12 @@ export default function SnsPage() {
|
|||||||
? `${overviewStats.totalFriends} 位好友`
|
? `${overviewStats.totalFriends} 位好友`
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
selectedUsernames={selectedUsernames}
|
|
||||||
setSelectedUsernames={setSelectedUsernames}
|
|
||||||
contacts={contacts}
|
contacts={contacts}
|
||||||
contactSearch={contactSearch}
|
contactSearch={contactSearch}
|
||||||
setContactSearch={setContactSearch}
|
setContactSearch={setContactSearch}
|
||||||
loading={contactsLoading}
|
loading={contactsLoading}
|
||||||
contactsCountProgress={contactsCountProgress}
|
contactsCountProgress={contactsCountProgress}
|
||||||
|
onOpenContactTimeline={openContactTimeline}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Dialogs and Overlays */}
|
{/* Dialogs and Overlays */}
|
||||||
@@ -1437,17 +1434,10 @@ export default function SnsPage() {
|
|||||||
|
|
||||||
<div className="export-dialog-body">
|
<div className="export-dialog-body">
|
||||||
{/* 筛选条件提示 */}
|
{/* 筛选条件提示 */}
|
||||||
{(selectedUsernames.length > 0 || searchKeyword) && (
|
{searchKeyword && (
|
||||||
<div className="export-filter-info">
|
<div className="export-filter-info">
|
||||||
<span className="filter-badge">筛选导出</span>
|
<span className="filter-badge">筛选导出</span>
|
||||||
{searchKeyword && <span className="filter-tag">关键词: "{searchKeyword}"</span>}
|
{searchKeyword && <span className="filter-tag">关键词: "{searchKeyword}"</span>}
|
||||||
{selectedUsernames.length > 0 && (
|
|
||||||
<span className="filter-tag">
|
|
||||||
<Users size={12} />
|
|
||||||
{selectedUsernames.length} 个联系人
|
|
||||||
<span className="sync-hint">(同步自侧栏筛选)</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -1584,7 +1574,7 @@ export default function SnsPage() {
|
|||||||
{/* 同步提示 */}
|
{/* 同步提示 */}
|
||||||
<div className="export-sync-hint">
|
<div className="export-sync-hint">
|
||||||
<Info size={14} />
|
<Info size={14} />
|
||||||
<span>将同步主页面的联系人范围筛选及关键词搜索</span>
|
<span>将同步主页面的关键词搜索</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 进度条 */}
|
{/* 进度条 */}
|
||||||
@@ -1626,7 +1616,6 @@ export default function SnsPage() {
|
|||||||
const result = await window.electronAPI.sns.exportTimeline({
|
const result = await window.electronAPI.sns.exportTimeline({
|
||||||
outputDir: exportFolder,
|
outputDir: exportFolder,
|
||||||
format: exportFormat,
|
format: exportFormat,
|
||||||
usernames: selectedUsernames.length > 0 ? selectedUsernames : undefined,
|
|
||||||
keyword: searchKeyword || undefined,
|
keyword: searchKeyword || undefined,
|
||||||
exportImages,
|
exportImages,
|
||||||
exportLivePhotos,
|
exportLivePhotos,
|
||||||
|
|||||||
Reference in New Issue
Block a user