agent-ecosystem/src/renderer/hooks/useViewportCommentRead.ts
iliya edddf526db refactor: reorganize task completion notifications in settings
- Moved the Task Completion Notifications section within the NotificationsSection component for improved structure and clarity.
- Updated the UI elements related to task completion notifications, ensuring consistent styling and functionality.
- Enhanced the overall user experience by maintaining clear access to notification settings.
2026-03-15 14:40:48 +02:00

90 lines
2.8 KiB
TypeScript

import { useCallback, useEffect, useRef } from 'react';
import { markAsRead } from '@renderer/services/commentReadStorage';
import { useViewportObserver } from './useViewportObserver';
import type { RefObject } from 'react';
interface UseViewportCommentReadOptions {
teamName: string;
taskId: string;
/**
* Scrollable ancestor element (e.g. DialogContent) used as IO root.
* Required for portalled Dialogs where the default viewport root
* would not detect intersections correctly.
*/
scrollContainerRef: RefObject<HTMLElement | null>;
}
/**
* Marks task comments as read based on viewport visibility.
*
* Instead of marking all comments read on mount, this hook uses
* IntersectionObserver (via useViewportObserver) to detect which
* comment elements are visible in the scroll container and updates
* the per-task read timestamp to the newest visible comment.
*
* Each comment element should be registered via the returned
* `registerComment(commentTimestampMs)` ref callback.
*
* Compatible with the existing per-task timestamp storage format
* in commentReadStorage — no storage schema changes needed.
*/
export function useViewportCommentRead({
teamName,
taskId,
scrollContainerRef,
}: UseViewportCommentReadOptions): {
/** Ref callback factory. Call with the comment's createdAt timestamp (ms). */
registerComment: (timestampMs: number) => (el: HTMLElement | null) => void;
/**
* Flush the highest observed timestamp now. Call on dialog close
* as a safety fallback (e.g. if IO did not fire for portal reasons).
*/
flush: () => void;
} {
const highestSeenRef = useRef(0);
const teamNameRef = useRef(teamName);
const taskIdRef = useRef(taskId);
teamNameRef.current = teamName;
taskIdRef.current = taskId;
// Reset tracked state when team/task changes
useEffect(() => {
highestSeenRef.current = 0;
}, [teamName, taskId]);
const handleVisibleChange = useCallback((visibleValues: string[]) => {
let maxTs = 0;
for (const v of visibleValues) {
const ts = Number(v);
if (Number.isFinite(ts) && ts > maxTs) {
maxTs = ts;
}
}
if (maxTs > 0 && maxTs > highestSeenRef.current) {
highestSeenRef.current = maxTs;
markAsRead(teamNameRef.current, taskIdRef.current, maxTs);
}
}, []);
const { registerElement } = useViewportObserver({
rootRef: scrollContainerRef,
threshold: 0.1,
onVisibleChange: handleVisibleChange,
});
const registerComment = useCallback(
(timestampMs: number) => registerElement(String(timestampMs)),
[registerElement]
);
const flush = useCallback(() => {
if (highestSeenRef.current > 0) {
markAsRead(teamNameRef.current, taskIdRef.current, highestSeenRef.current);
}
}, []);
return { registerComment, flush };
}