diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 2dfe880c..35cbfd92 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -31,7 +31,7 @@ import { TeamSessionsSection } from './TeamSessionsSection'; import type { KanbanFilterState } from './kanban/KanbanFilterPopover'; import type { MessagesFilterState } from './messages/MessagesFilterPopover'; import type { Session } from '@renderer/types/data'; -import type { ResolvedTeamMember, TeamTask } from '@shared/types'; +import type { InboxMessage, ResolvedTeamMember, TeamTask } from '@shared/types'; interface TeamDetailViewProps { teamName: string; @@ -294,6 +294,10 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele () => filteredMessages.filter((m) => !readSet.has(toMessageKey(m))).length, [filteredMessages, readSet] ); + const handleMessageVisible = useCallback( + (message: InboxMessage) => markRead(toMessageKey(message)), + [markRead] + ); const kanbanDisplayTasks = useMemo(() => { const query = kanbanSearch.trim(); @@ -691,7 +695,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele setReplyQuote({ from: message.from, text: message.text }); setSendDialogOpen(true); }} - onMessageVisible={(message) => markRead(toMessageKey(message))} + onMessageVisible={handleMessageVisible} /> diff --git a/src/renderer/components/team/activity/ActivityTimeline.tsx b/src/renderer/components/team/activity/ActivityTimeline.tsx index 675ad36e..f15cb7f4 100644 --- a/src/renderer/components/team/activity/ActivityTimeline.tsx +++ b/src/renderer/components/team/activity/ActivityTimeline.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useEffect, useRef } from 'react'; import { getMemberColorByName } from '@shared/constants/memberColors'; @@ -39,16 +39,10 @@ const MessageRowWithObserver = ({ }): React.JSX.Element => { const ref = useRef(null); const reportedRef = useRef(false); - - const handleIntersect = useCallback( - (entry: IntersectionObserverEntry) => { - if (!entry.isIntersecting || !onVisible) return; - if (reportedRef.current) return; - reportedRef.current = true; - onVisible(message); - }, - [message, onVisible] - ); + const messageRef = useRef(message); + const onVisibleRef = useRef(onVisible); + messageRef.current = message; + onVisibleRef.current = onVisible; useEffect(() => { if (!onVisible) return; @@ -56,13 +50,19 @@ const MessageRowWithObserver = ({ if (!el) return; const observer = new IntersectionObserver( ([entry]) => { - if (entry) handleIntersect(entry); + if (!entry?.isIntersecting) return; + if (reportedRef.current) return; + const cb = onVisibleRef.current; + const msg = messageRef.current; + if (!cb) return; + reportedRef.current = true; + cb(msg); }, { threshold: VIEWPORT_THRESHOLD, rootMargin: '0px' } ); observer.observe(el); return () => observer.disconnect(); - }, [onVisible, handleIntersect]); + }, [onVisible]); return (
diff --git a/src/renderer/components/team/dialogs/TaskCommentsSection.tsx b/src/renderer/components/team/dialogs/TaskCommentsSection.tsx index 7a8223c1..f09f150d 100644 --- a/src/renderer/components/team/dialogs/TaskCommentsSection.tsx +++ b/src/renderer/components/team/dialogs/TaskCommentsSection.tsx @@ -116,18 +116,6 @@ export const TaskCommentsSection = ({ : formatDistanceToNow(date, { addSuffix: true }); })()} -
-
- {(() => { - const reply = parseMessageReply(comment.text); - const expanded = expandedCommentIds.has(comment.id); - return reply ? ( - - ) : ( - - ); - })()} -
+ {(() => { + const reply = parseMessageReply(comment.text); + const expanded = expandedCommentIds.has(comment.id); + const collapsedHeight = 'max-h-[120px]'; + return ( +
+
+ {reply ? ( + + ) : ( + + )} + {!expanded && ( + <> +
+
+ +
+ + )} +
+ {expanded && ( +
+ +
+ )} +
+ ); + })()}
))} diff --git a/src/renderer/hooks/useTeamMessagesRead.ts b/src/renderer/hooks/useTeamMessagesRead.ts index 7b2ec59c..95a6eca1 100644 --- a/src/renderer/hooks/useTeamMessagesRead.ts +++ b/src/renderer/hooks/useTeamMessagesRead.ts @@ -28,7 +28,7 @@ export function useTeamMessagesRead(teamName: string): { if (prev.has(messageKey)) return prev; const next = new Set(prev); next.add(messageKey); - markReadStorage(teamName, messageKey); + markReadStorage(teamName, messageKey, next); return next; }); }, diff --git a/src/renderer/utils/teamMessageReadStorage.ts b/src/renderer/utils/teamMessageReadStorage.ts index a65a44ce..54772bda 100644 --- a/src/renderer/utils/teamMessageReadStorage.ts +++ b/src/renderer/utils/teamMessageReadStorage.ts @@ -16,12 +16,22 @@ export function getReadSet(teamName: string): Set { } } -export function markRead(teamName: string, messageKey: string): void { - const set = getReadSet(teamName); - if (set.has(messageKey)) return; - set.add(messageKey); +/** + * Mark a message as read and persist. If `fullSet` is provided, that set is written + * (avoids losing keys when a previous write failed). Otherwise reads from localStorage and adds one key. + */ +export function markRead(teamName: string, messageKey: string, fullSet?: Set): void { + const toWrite = + fullSet ?? + (() => { + const set = getReadSet(teamName); + if (set.has(messageKey)) return null; + set.add(messageKey); + return set; + })(); + if (!toWrite) return; try { - localStorage.setItem(storageKey(teamName), JSON.stringify([...set])); + localStorage.setItem(storageKey(teamName), JSON.stringify([...toWrite])); } catch { // quota or disabled }