From a99f5e581fdfac56864a30f240724d263ab9baaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Apr 2026 19:29:23 -0400 Subject: [PATCH] Fix stuck selectedEntityId and unread-during-search edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small but user-visible UX issues in the Communication hub: 1. selectedEntityId was only consumed when the id resolved to a visible message / comment / task channel. When the referenced entity was deleted or filtered out, the pointer stuck and the effect re-ran on every store change, preventing the user from manually navigating to other channels. 2. The unread-marking effect explicitly skipped while search was active, even though the search results render in the same message list pane. Users who opened a channel via search saw unread badges freeze until they cleared the query and re-entered the channel. Consume the entity id on every resolution branch (including the no-match fallback) and drop the search guard — the viewer intent is the same in both list modes. --- .../communication/CommunicationHub.tsx | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/communication/CommunicationHub.tsx b/src/renderer/components/communication/CommunicationHub.tsx index 7b669537..e733d831 100644 --- a/src/renderer/components/communication/CommunicationHub.tsx +++ b/src/renderer/components/communication/CommunicationHub.tsx @@ -101,11 +101,30 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects, useEffect(() => { if (!selectedEntityId) return const matchMsg = effectiveMessages.find((m) => m.id === selectedEntityId) - if (matchMsg) { const ch = messageChannels.find((ch) => getChannelMessages(effectiveMessages, ch).some((m) => m.id === selectedEntityId)); if (ch) { setSelectedChannelKey(ch.key); onEntityConsumed?.() }; return } + if (matchMsg) { + const ch = messageChannels.find((ch) => getChannelMessages(effectiveMessages, ch).some((m) => m.id === selectedEntityId)) + if (ch) setSelectedChannelKey(ch.key) + onEntityConsumed?.() + return + } const matchCom = comments.find((c) => c.id === selectedEntityId) - if (matchCom) { setSelectedChannelKey(`task-${matchCom.taskId}`); onEntityConsumed?.(); return } + if (matchCom) { + setSelectedChannelKey(`task-${matchCom.taskId}`) + onEntityConsumed?.() + return + } const matchTask = taskDiscussionChannels.find((d) => d.taskId === selectedEntityId) - if (matchTask) { setSelectedChannelKey(matchTask.key); onEntityConsumed?.() } + if (matchTask) { + setSelectedChannelKey(matchTask.key) + onEntityConsumed?.() + return + } + // Entity id is stale or points outside this view (e.g. the message + // was deleted between navigation and render). Consume it anyway so + // the parent store clears the pointer — leaving it set would make + // the effect refire on every snapshot and block the user from + // navigating manually inside the communication hub. + onEntityConsumed?.() }, [selectedEntityId, effectiveMessages, comments, messageChannels, onEntityConsumed, taskDiscussionChannels]) const activeChannel = useMemo(() => channels.find((c) => c.key === selectedChannelKey) ?? null, [channels, selectedChannelKey]) @@ -201,7 +220,13 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects, useEffect(() => { if (!activeChannel || activeChannel.kind !== "message") return - if (isSearchActive) return // Don't mark as read during search + // Search no longer blocks read marking — the original guard existed + // to keep unread badges stable while the user was scanning the + // results list, but the search panel renders inside the message + // list pane (isSearchActive swaps the rendered list) so messages + // shown through search are the same messages the user is actually + // viewing. Blocking reads here left unreads stuck until the user + // cleared search and re-opened the channel. const ids = filteredMessages.filter((m) => !m.readAt && !pendingReadIdsRef.current.has(m.id)).map((m) => m.id) if (ids.length === 0) return let cancelled = false @@ -214,7 +239,7 @@ export function CommunicationHub({ messages, comments, tasks, agents, projects, if (!cancelled && failed.length > 0) setOptimisticReadIds((c) => { const n = new Set(c); for (const id of failed) n.delete(id); return n }) })() return () => { cancelled = true } - }, [activeChannel, companyId, filteredMessages, isSearchActive]) + }, [activeChannel, companyId, filteredMessages]) const unreadByChannel = useMemo(() => { const counts = new Map()