'use client' import type { FC, PointerEvent as ReactPointerEvent } from 'react' import type { WorkflowCommentList } from '@/service/workflow-comment' import { memo, useCallback, useMemo, useRef, useState } from 'react' import { useReactFlow, useViewport } from 'reactflow' import { UserAvatarList } from '@/app/components/base/user-avatar-list' import { useAppContext } from '@/context/app-context' import CommentPreview from './comment-preview' type CommentIconProps = { comment: WorkflowCommentList onClick: () => void isActive?: boolean onPositionUpdate?: (position: { x: number, y: number }) => void } export const CommentIcon: FC = memo(({ comment, onClick, isActive = false, onPositionUpdate }) => { const { flowToScreenPosition, screenToFlowPosition } = useReactFlow() const viewport = useViewport() const { userProfile } = useAppContext() const isAuthor = comment.created_by_account?.id === userProfile?.id const [showPreview, setShowPreview] = useState(false) const [dragPosition, setDragPosition] = useState<{ x: number, y: number } | null>(null) const [isDragging, setIsDragging] = useState(false) const dragStateRef = useRef<{ offsetX: number offsetY: number startX: number startY: number hasMoved: boolean } | null>(null) const workflowContainerRect = typeof document !== 'undefined' ? document.getElementById('workflow-container')?.getBoundingClientRect() : null const containerLeft = workflowContainerRect?.left ?? 0 const containerTop = workflowContainerRect?.top ?? 0 const screenPosition = useMemo(() => { return flowToScreenPosition({ x: comment.position_x, y: comment.position_y, }) }, [comment.position_x, comment.position_y, viewport.x, viewport.y, viewport.zoom, flowToScreenPosition]) const effectiveScreenPosition = dragPosition ?? screenPosition const canvasPosition = useMemo(() => ({ x: effectiveScreenPosition.x - containerLeft, y: effectiveScreenPosition.y - containerTop, }), [effectiveScreenPosition.x, effectiveScreenPosition.y, containerLeft, containerTop]) const cursorClass = useMemo(() => { if (!isAuthor) return 'cursor-pointer' if (isActive) return isDragging ? 'cursor-grabbing' : '' return isDragging ? 'cursor-grabbing' : 'cursor-pointer' }, [isActive, isAuthor, isDragging]) const handlePointerDown = useCallback((event: ReactPointerEvent) => { if (event.button !== 0) return event.stopPropagation() event.preventDefault() if (!isAuthor) { if (event.currentTarget.dataset.role !== 'comment-preview') setShowPreview(false) return } dragStateRef.current = { offsetX: event.clientX - screenPosition.x, offsetY: event.clientY - screenPosition.y, startX: event.clientX, startY: event.clientY, hasMoved: false, } setDragPosition(screenPosition) setIsDragging(false) if (event.currentTarget.dataset.role !== 'comment-preview') setShowPreview(false) if (event.currentTarget.setPointerCapture) event.currentTarget.setPointerCapture(event.pointerId) }, [isAuthor, screenPosition]) const handlePointerMove = useCallback((event: ReactPointerEvent) => { const dragState = dragStateRef.current if (!dragState) return event.stopPropagation() event.preventDefault() const nextX = event.clientX - dragState.offsetX const nextY = event.clientY - dragState.offsetY if (!dragState.hasMoved) { const distance = Math.hypot(event.clientX - dragState.startX, event.clientY - dragState.startY) if (distance > 4) { dragState.hasMoved = true setIsDragging(true) } } setDragPosition({ x: nextX, y: nextY }) }, []) const finishDrag = useCallback((event: ReactPointerEvent) => { const dragState = dragStateRef.current if (!dragState) return false if (event.currentTarget.hasPointerCapture?.(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId) dragStateRef.current = null setDragPosition(null) setIsDragging(false) return dragState.hasMoved }, []) const handlePointerUp = useCallback((event: ReactPointerEvent) => { event.stopPropagation() event.preventDefault() const finalScreenPosition = dragPosition ?? screenPosition const didDrag = finishDrag(event) setShowPreview(false) if (didDrag) { if (onPositionUpdate) { const flowPosition = screenToFlowPosition({ x: finalScreenPosition.x, y: finalScreenPosition.y, }) onPositionUpdate(flowPosition) } } else if (!isActive) { onClick() } }, [dragPosition, finishDrag, isActive, onClick, onPositionUpdate, screenPosition, screenToFlowPosition]) const handlePointerCancel = useCallback((event: ReactPointerEvent) => { event.stopPropagation() event.preventDefault() finishDrag(event) }, [finishDrag]) const handleMouseEnter = useCallback(() => { if (isActive || isDragging) return setShowPreview(true) }, [isActive, isDragging]) const handleMouseLeave = useCallback(() => { setShowPreview(false) }, []) const participants = useMemo(() => { const list = comment.participants ?? [] const author = comment.created_by_account if (!author) return [...list] const rest = list.filter(user => user.id !== author.id) return [author, ...rest] }, [comment.created_by_account, comment.participants]) // Calculate dynamic width based on number of participants const participantCount = participants.length const maxVisible = Math.min(3, participantCount) const showCount = participantCount > 3 const avatarSize = 24 const avatarSpacing = 4 // -space-x-1 is about 4px overlap // Width calculation: first avatar + (additional avatars * (size - spacing)) + padding const dynamicWidth = Math.max(40, // minimum width 8 + avatarSize + Math.max(0, (showCount ? 2 : maxVisible - 1)) * (avatarSize - avatarSpacing) + 8) const pointerEventHandlers = useMemo(() => ({ onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel, }), [handlePointerCancel, handlePointerDown, handlePointerMove, handlePointerUp]) return ( <>
{/* Preview panel */} {showPreview && !isActive && (
setShowPreview(true)} onMouseLeave={() => setShowPreview(false)} > { setShowPreview(false) onClick() }} />
)} ) }, (prevProps, nextProps) => { return ( prevProps.comment.id === nextProps.comment.id && prevProps.comment.position_x === nextProps.comment.position_x && prevProps.comment.position_y === nextProps.comment.position_y && prevProps.onClick === nextProps.onClick && prevProps.isActive === nextProps.isActive && prevProps.onPositionUpdate === nextProps.onPositionUpdate ) }) CommentIcon.displayName = 'CommentIcon'