mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat: filter SNS feed by selected contacts
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
/* Global Variables */
|
/* Global Variables */
|
||||||
:root {
|
:root {
|
||||||
--sns-max-width: 800px;
|
--sns-max-width: 800px;
|
||||||
--sns-panel-width: 320px;
|
--sns-panel-width: 380px;
|
||||||
--sns-bg-color: var(--bg-primary);
|
--sns-bg-color: var(--bg-primary);
|
||||||
--sns-card-bg: var(--bg-secondary);
|
--sns-card-bg: var(--bg-secondary);
|
||||||
--sns-border-radius-lg: 16px;
|
--sns-border-radius-lg: 16px;
|
||||||
@@ -263,6 +263,48 @@
|
|||||||
padding-top: 16px;
|
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 {
|
.posts-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -173,9 +173,11 @@ export default function SnsPage() {
|
|||||||
const overviewStatsStatusRef = useRef<OverviewStatsStatus>(overviewStatsStatus)
|
const overviewStatsStatusRef = useRef<OverviewStatsStatus>(overviewStatsStatus)
|
||||||
const searchKeywordRef = useRef(searchKeyword)
|
const searchKeywordRef = useRef(searchKeyword)
|
||||||
const jumpTargetDateRef = useRef<Date | undefined>(jumpTargetDate)
|
const jumpTargetDateRef = useRef<Date | undefined>(jumpTargetDate)
|
||||||
|
const selectedContactUsernamesRef = useRef<string[]>(selectedContactUsernames)
|
||||||
const cacheScopeKeyRef = useRef('')
|
const cacheScopeKeyRef = useRef('')
|
||||||
const snsUserPostCountsCacheScopeKeyRef = useRef('')
|
const snsUserPostCountsCacheScopeKeyRef = useRef('')
|
||||||
const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null)
|
const scrollAdjustmentRef = useRef<{ scrollHeight: number; scrollTop: number } | null>(null)
|
||||||
|
const pendingResetFeedRef = useRef(false)
|
||||||
const contactsLoadTokenRef = useRef(0)
|
const contactsLoadTokenRef = useRef(0)
|
||||||
const contactsCountHydrationTokenRef = useRef(0)
|
const contactsCountHydrationTokenRef = useRef(0)
|
||||||
const contactsCountBatchTimerRef = useRef<number | null>(null)
|
const contactsCountBatchTimerRef = useRef<number | null>(null)
|
||||||
@@ -208,6 +210,9 @@ export default function SnsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
jumpTargetDateRef.current = jumpTargetDate
|
jumpTargetDateRef.current = jumpTargetDate
|
||||||
}, [jumpTargetDate])
|
}, [jumpTargetDate])
|
||||||
|
useEffect(() => {
|
||||||
|
selectedContactUsernamesRef.current = selectedContactUsernames
|
||||||
|
}, [selectedContactUsernames])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showJumpPopover) {
|
if (!showJumpPopover) {
|
||||||
setJumpPopoverDate(jumpTargetDate || new Date())
|
setJumpPopoverDate(jumpTargetDate || new Date())
|
||||||
@@ -394,6 +399,14 @@ export default function SnsPage() {
|
|||||||
return `${names.slice(0, 2).join('、')} 等 ${names.length} 位联系人`
|
return `${names.slice(0, 2).join('、')} 等 ${names.length} 位联系人`
|
||||||
}, [contacts, exportScope])
|
}, [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(() => {
|
const myTimelineCount = useMemo(() => {
|
||||||
if (resolvedCurrentUserContact?.postCountStatus === 'ready' && typeof resolvedCurrentUserContact.postCount === 'number') {
|
if (resolvedCurrentUserContact?.postCountStatus === 'ready' && typeof resolvedCurrentUserContact.postCount === 'number') {
|
||||||
return normalizePostCount(resolvedCurrentUserContact.postCount)
|
return normalizePostCount(resolvedCurrentUserContact.postCount)
|
||||||
@@ -421,7 +434,11 @@ export default function SnsPage() {
|
|||||||
}, [currentUserProfile.avatarUrl, currentUserProfile.displayName, resolvedCurrentUserContact])
|
}, [currentUserProfile.avatarUrl, currentUserProfile.displayName, resolvedCurrentUserContact])
|
||||||
|
|
||||||
const isDefaultViewNow = useCallback(() => {
|
const isDefaultViewNow = useCallback(() => {
|
||||||
return !searchKeywordRef.current.trim() && !jumpTargetDateRef.current
|
return (
|
||||||
|
!searchKeywordRef.current.trim() &&
|
||||||
|
!jumpTargetDateRef.current &&
|
||||||
|
selectedContactUsernamesRef.current.length === 0
|
||||||
|
)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const ensureSnsCacheScopeKey = useCallback(async () => {
|
const ensureSnsCacheScopeKey = useCallback(async () => {
|
||||||
@@ -594,7 +611,12 @@ export default function SnsPage() {
|
|||||||
|
|
||||||
const loadPosts = useCallback(async (options: { reset?: boolean, direction?: 'older' | 'newer' } = {}) => {
|
const loadPosts = useCallback(async (options: { reset?: boolean, direction?: 'older' | 'newer' } = {}) => {
|
||||||
const { reset = false, direction = 'older' } = options
|
const { reset = false, direction = 'older' } = options
|
||||||
if (loadingRef.current) return
|
if (loadingRef.current) {
|
||||||
|
if (reset) {
|
||||||
|
pendingResetFeedRef.current = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
loadingRef.current = true
|
loadingRef.current = true
|
||||||
if (direction === 'newer') setLoadingNewer(true)
|
if (direction === 'newer') setLoadingNewer(true)
|
||||||
@@ -602,6 +624,7 @@ export default function SnsPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const limit = 20
|
const limit = 20
|
||||||
|
const selectedUsernames = selectedContactUsernames.length > 0 ? selectedContactUsernames : undefined
|
||||||
let startTs: number | undefined = undefined
|
let startTs: number | undefined = undefined
|
||||||
let endTs: number | undefined = undefined
|
let endTs: number | undefined = undefined
|
||||||
|
|
||||||
@@ -618,7 +641,7 @@ export default function SnsPage() {
|
|||||||
const result = await window.electronAPI.sns.getTimeline(
|
const result = await window.electronAPI.sns.getTimeline(
|
||||||
limit,
|
limit,
|
||||||
0,
|
0,
|
||||||
undefined,
|
selectedUsernames,
|
||||||
searchKeyword,
|
searchKeyword,
|
||||||
topTs + 1,
|
topTs + 1,
|
||||||
undefined
|
undefined
|
||||||
@@ -659,7 +682,7 @@ export default function SnsPage() {
|
|||||||
const result = await window.electronAPI.sns.getTimeline(
|
const result = await window.electronAPI.sns.getTimeline(
|
||||||
limit,
|
limit,
|
||||||
0,
|
0,
|
||||||
undefined,
|
selectedUsernames,
|
||||||
searchKeyword,
|
searchKeyword,
|
||||||
startTs, // default undefined
|
startTs, // default undefined
|
||||||
endTs
|
endTs
|
||||||
@@ -674,7 +697,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, 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));
|
setHasNewer(!!(checkResult.success && checkResult.timeline && checkResult.timeline.length > 0));
|
||||||
} else {
|
} else {
|
||||||
setHasNewer(false);
|
setHasNewer(false);
|
||||||
@@ -700,8 +723,12 @@ export default function SnsPage() {
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
setLoadingNewer(false)
|
setLoadingNewer(false)
|
||||||
loadingRef.current = 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) => {
|
const stopContactsCountHydration = useCallback((resetProgress = false) => {
|
||||||
contactsCountHydrationTokenRef.current += 1
|
contactsCountHydrationTokenRef.current += 1
|
||||||
@@ -1155,6 +1182,7 @@ export default function SnsPage() {
|
|||||||
stopContactsCountHydration(true)
|
stopContactsCountHydration(true)
|
||||||
setContacts([])
|
setContacts([])
|
||||||
setPosts([]); setHasMore(true); setHasNewer(false);
|
setPosts([]); setHasMore(true); setHasNewer(false);
|
||||||
|
setSelectedContactUsernames([])
|
||||||
setSearchKeyword(''); setJumpTargetDate(undefined);
|
setSearchKeyword(''); setJumpTargetDate(undefined);
|
||||||
void hydrateSnsPageCache()
|
void hydrateSnsPageCache()
|
||||||
loadContacts();
|
loadContacts();
|
||||||
@@ -1172,6 +1200,21 @@ export default function SnsPage() {
|
|||||||
return () => clearTimeout(timer)
|
return () => clearTimeout(timer)
|
||||||
}, [searchKeyword, jumpTargetDate, loadPosts])
|
}, [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<HTMLDivElement>) => {
|
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||||
const { scrollTop, clientHeight, scrollHeight } = e.currentTarget
|
const { scrollTop, clientHeight, scrollHeight } = e.currentTarget
|
||||||
if (scrollHeight - scrollTop - clientHeight < 400 && hasMore && !loading && !loadingNewer) {
|
if (scrollHeight - scrollTop - clientHeight < 400 && hasMore && !loading && !loadingNewer) {
|
||||||
@@ -1334,6 +1377,20 @@ export default function SnsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{selectedContactUsernames.length > 0 && (
|
||||||
|
<div className="feed-contact-filter-bar">
|
||||||
|
<span className="feed-contact-filter-label">仅显示</span>
|
||||||
|
<span className="feed-contact-filter-summary">{selectedFeedContactsSummary} 的动态</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="feed-contact-filter-clear"
|
||||||
|
onClick={clearSelectedContacts}
|
||||||
|
>
|
||||||
|
清空筛选
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="sns-posts-scroll" onScroll={handleScroll} onWheel={handleWheel} ref={postsContainerRef}>
|
<div className="sns-posts-scroll" onScroll={handleScroll} onWheel={handleWheel} ref={postsContainerRef}>
|
||||||
{loadingNewer && (
|
{loadingNewer && (
|
||||||
<div className="status-indicator loading-newer">
|
<div className="status-indicator loading-newer">
|
||||||
@@ -1391,9 +1448,11 @@ export default function SnsPage() {
|
|||||||
<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>
|
||||||
{(searchKeyword || jumpTargetDate) && (
|
{(searchKeyword || jumpTargetDate || selectedContactUsernames.length > 0) && (
|
||||||
<button onClick={() => {
|
<button onClick={() => {
|
||||||
setSearchKeyword(''); setJumpTargetDate(undefined);
|
setSearchKeyword('')
|
||||||
|
setJumpTargetDate(undefined)
|
||||||
|
clearSelectedContacts()
|
||||||
}} className="reset-inline">
|
}} className="reset-inline">
|
||||||
重置筛选条件
|
重置筛选条件
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user