From c02bc753fd1617ea2b0777384130da0f29c9dfce Mon Sep 17 00:00:00 2001 From: aits2026 Date: Tue, 10 Mar 2026 11:38:38 +0800 Subject: [PATCH] feat: filter SNS feed by selected contacts --- src/pages/SnsPage.scss | 44 ++++++++++++++++++++++++- src/pages/SnsPage.tsx | 75 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 110 insertions(+), 9 deletions(-) diff --git a/src/pages/SnsPage.scss b/src/pages/SnsPage.scss index 486c23a..45fcfbc 100644 --- a/src/pages/SnsPage.scss +++ b/src/pages/SnsPage.scss @@ -1,7 +1,7 @@ /* Global Variables */ :root { --sns-max-width: 800px; - --sns-panel-width: 320px; + --sns-panel-width: 380px; --sns-bg-color: var(--bg-primary); --sns-card-bg: var(--bg-secondary); --sns-border-radius-lg: 16px; @@ -263,6 +263,48 @@ padding-top: 16px; } +.feed-contact-filter-bar { + margin: 10px 4px 0; + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--primary) 28%, var(--border-color)); + border-radius: 12px; + background: rgba(var(--primary-rgb), 0.08); + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + + .feed-contact-filter-label { + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; + } + + .feed-contact-filter-summary { + font-size: 13px; + color: var(--text-primary); + font-weight: 600; + min-width: 0; + } + + .feed-contact-filter-clear { + margin-left: auto; + border: none; + background: transparent; + color: var(--primary); + font-size: 12px; + font-weight: 600; + cursor: pointer; + padding: 0; + white-space: nowrap; + + &:hover { + text-decoration: underline; + text-underline-offset: 2px; + } + } +} + .posts-list { display: flex; flex-direction: column; diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index 23ad458..f8ac599 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -173,9 +173,11 @@ export default function SnsPage() { const overviewStatsStatusRef = useRef(overviewStatsStatus) const searchKeywordRef = useRef(searchKeyword) const jumpTargetDateRef = useRef(jumpTargetDate) + const selectedContactUsernamesRef = useRef(selectedContactUsernames) const cacheScopeKeyRef = useRef('') const snsUserPostCountsCacheScopeKeyRef = useRef('') const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null) + const pendingResetFeedRef = useRef(false) const contactsLoadTokenRef = useRef(0) const contactsCountHydrationTokenRef = useRef(0) const contactsCountBatchTimerRef = useRef(null) @@ -208,6 +210,9 @@ export default function SnsPage() { useEffect(() => { jumpTargetDateRef.current = jumpTargetDate }, [jumpTargetDate]) + useEffect(() => { + selectedContactUsernamesRef.current = selectedContactUsernames + }, [selectedContactUsernames]) useEffect(() => { if (!showJumpPopover) { setJumpPopoverDate(jumpTargetDate || new Date()) @@ -394,6 +399,14 @@ export default function SnsPage() { return `${names.slice(0, 2).join('、')} 等 ${names.length} 位联系人` }, [contacts, exportScope]) + const selectedFeedContactsSummary = useMemo(() => { + if (selectedContactUsernames.length === 0) return '' + const contactMap = new Map(contacts.map((contact) => [contact.username, contact])) + const names = selectedContactUsernames.map((username) => contactMap.get(username)?.displayName || username) + if (names.length <= 2) return names.join('、') + return `${names.slice(0, 2).join('、')} 等 ${names.length} 人` + }, [contacts, selectedContactUsernames]) + const myTimelineCount = useMemo(() => { if (resolvedCurrentUserContact?.postCountStatus === 'ready' && typeof resolvedCurrentUserContact.postCount === 'number') { return normalizePostCount(resolvedCurrentUserContact.postCount) @@ -421,7 +434,11 @@ export default function SnsPage() { }, [currentUserProfile.avatarUrl, currentUserProfile.displayName, resolvedCurrentUserContact]) const isDefaultViewNow = useCallback(() => { - return !searchKeywordRef.current.trim() && !jumpTargetDateRef.current + return ( + !searchKeywordRef.current.trim() && + !jumpTargetDateRef.current && + selectedContactUsernamesRef.current.length === 0 + ) }, []) const ensureSnsCacheScopeKey = useCallback(async () => { @@ -594,7 +611,12 @@ export default function SnsPage() { const loadPosts = useCallback(async (options: { reset?: boolean, direction?: 'older' | 'newer' } = {}) => { const { reset = false, direction = 'older' } = options - if (loadingRef.current) return + if (loadingRef.current) { + if (reset) { + pendingResetFeedRef.current = true + } + return + } loadingRef.current = true if (direction === 'newer') setLoadingNewer(true) @@ -602,6 +624,7 @@ export default function SnsPage() { try { const limit = 20 + const selectedUsernames = selectedContactUsernames.length > 0 ? selectedContactUsernames : undefined let startTs: number | undefined = undefined let endTs: number | undefined = undefined @@ -618,7 +641,7 @@ export default function SnsPage() { const result = await window.electronAPI.sns.getTimeline( limit, 0, - undefined, + selectedUsernames, searchKeyword, topTs + 1, undefined @@ -659,7 +682,7 @@ export default function SnsPage() { const result = await window.electronAPI.sns.getTimeline( limit, 0, - undefined, + selectedUsernames, searchKeyword, startTs, // default undefined endTs @@ -674,7 +697,7 @@ export default function SnsPage() { // Check for newer items above topTs const topTs = result.timeline[0]?.createTime || 0; if (topTs > 0) { - const checkResult = await window.electronAPI.sns.getTimeline(1, 0, undefined, searchKeyword, topTs + 1, undefined); + 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); @@ -700,8 +723,12 @@ export default function SnsPage() { setLoading(false) setLoadingNewer(false) loadingRef.current = false + if (pendingResetFeedRef.current) { + pendingResetFeedRef.current = false + void loadPosts({ reset: true }) + } } - }, [jumpTargetDate, persistSnsPageCache, searchKeyword]) + }, [jumpTargetDate, persistSnsPageCache, searchKeyword, selectedContactUsernames]) const stopContactsCountHydration = useCallback((resetProgress = false) => { contactsCountHydrationTokenRef.current += 1 @@ -1155,6 +1182,7 @@ export default function SnsPage() { stopContactsCountHydration(true) setContacts([]) setPosts([]); setHasMore(true); setHasNewer(false); + setSelectedContactUsernames([]) setSearchKeyword(''); setJumpTargetDate(undefined); void hydrateSnsPageCache() loadContacts(); @@ -1172,6 +1200,21 @@ export default function SnsPage() { return () => clearTimeout(timer) }, [searchKeyword, jumpTargetDate, loadPosts]) + const selectedContactUsernamesKey = useMemo( + () => selectedContactUsernames.join('||'), + [selectedContactUsernames] + ) + + const hasInitializedSelectedFeedFilterRef = useRef(false) + + useEffect(() => { + if (!hasInitializedSelectedFeedFilterRef.current) { + hasInitializedSelectedFeedFilterRef.current = true + return + } + loadPosts({ reset: true }) + }, [loadPosts, selectedContactUsernamesKey]) + const handleScroll = (e: React.UIEvent) => { const { scrollTop, clientHeight, scrollHeight } = e.currentTarget if (scrollHeight - scrollTop - clientHeight < 400 && hasMore && !loading && !loadingNewer) { @@ -1334,6 +1377,20 @@ export default function SnsPage() { + {selectedContactUsernames.length > 0 && ( +
+ 仅显示 + {selectedFeedContactsSummary} 的动态 + +
+ )} +
{loadingNewer && (
@@ -1391,9 +1448,11 @@ export default function SnsPage() {

未找到相关动态

- {(searchKeyword || jumpTargetDate) && ( + {(searchKeyword || jumpTargetDate || selectedContactUsernames.length > 0) && (