import React, { useEffect, useRef, useState } from "react"; import { CloseIcon, KebabIcon } from "../../../icons/ExportCommonIcons"; import Messages from "./Messages"; import { ExpandIcon } from "../../../icons/SimulationIcons"; import { adjustHeight } from "../function/textAreaHeightAdjust"; import { useParams } from "react-router-dom"; import { useSelectedComment, useSocketStore } from "../../../../store/builder/store"; import { getUserData } from "../../../../functions/getUserData"; import ThreadSocketResponsesDev from "../../../../modules/collaboration/socket/threadSocketResponses.dev"; import { useSceneContext } from "../../../../modules/scene/sceneContext"; import { addCommentsApi } from "../../../../services/factoryBuilder/collab/comments/addCommentApi"; import { deleteThreadApi } from "../../../../services/factoryBuilder/collab/comments/deleteThreadApi"; import { createThreadApi } from "../../../../services/factoryBuilder/collab/comments/createThreadApi"; import { getRelativeTime } from "../function/getRelativeTime"; const ThreadChat: React.FC = () => { const { userId, organization } = getUserData(); const [openThreadOptions, setOpenThreadOptions] = useState(false); const [inputActive, setInputActive] = useState(false); const [value, setValue] = useState(""); const { selectedComment, setSelectedComment, setCommentPositionState, commentPositionState, position2Dstate } = useSelectedComment(); const [mode, setMode] = useState<"create" | "edit" | null>("create"); const [isEditable, setIsEditable] = useState(false); const [editedThread, setEditedThread] = useState(false); const textareaRef = useRef(null); const wrapperRef = useRef(null); const [messages, setMessages] = useState([]); const { projectId } = useParams(); const [dragging, setDragging] = useState(false); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [position, setPosition] = useState({ x: position2Dstate.x, y: position2Dstate.y }); const { threadSocket } = useSocketStore(); const modeRef = useRef<"create" | "edit" | null>(null); const messagesRef = useRef(null); const { versionStore, threadStore } = useSceneContext(); const { selectedVersion } = versionStore(); const { addComment, removeComment, addReply, threads } = threadStore(); useEffect(() => { modeRef.current = mode; }, [mode]); useEffect(() => { if (threads.length > 0 && selectedComment) { const allMessages = threads .flatMap((val: any) => (val?.threadId === selectedComment?.threadId ? val.comments : [])) .map((c) => { return { replyId: c._id ?? c.replyId, creatorId: c.creatorId || c.userId, createdAt: c.createdAt, lastUpdatedAt: "1 hr ago", comment: c.comment, _id: c._id ?? c.replyId, }; }); setMessages(allMessages); } }, [selectedComment]); useEffect(() => { if (textareaRef.current) adjustHeight(textareaRef.current); }, [value]); const clamp = (val: number, min: number, max: number) => { return Math.min(Math.max(val, min), max); }; const handlePointerDown = (event: React.PointerEvent) => { if (event.button !== 0) return; const target = event.target as HTMLElement; if ( target.closest("button") || target.closest(".sent-button") || target.closest("textarea") || target.closest(".options-button") || target.closest(".options-list") || target.closest(".send-message-wrapper") || target.closest(".options delete") ) { return; } const wrapper = wrapperRef.current; if (!wrapper) return; const rect = wrapper.getBoundingClientRect(); const offsetX = event.clientX - rect.left; const offsetY = event.clientY - rect.top; setDragging(true); setDragOffset({ x: offsetX, y: offsetY }); wrapper.setPointerCapture(event.pointerId); }; const updatePosition = ({ clientX, clientY }: { clientX: number; clientY: number }, allowMove: boolean = true) => { if (!allowMove || !wrapperRef.current) return; const container = document.getElementById("work-space-three-d-canvas"); const wrapper = wrapperRef.current; if (!container || !wrapper) return; const containerRect = container.getBoundingClientRect(); let newX = clientX - containerRect.left - dragOffset.x; let newY = clientY - containerRect.top - dragOffset.y; const maxX = containerRect.width - wrapper.offsetWidth; const maxY = containerRect.height - wrapper.offsetHeight; newX = clamp(newX, 0, maxX); newY = clamp(newY, 0, maxY); setPosition({ x: newX, y: newY }); }; const handlePointerMove = (e: { clientX: number; clientY: number }) => { if (dragging) updatePosition(e, true); }; // Commented this useEffect to prevent offset after user saved the comment // useEffect(() => { // updatePosition({ clientX: position.x, clientY: position.y }, true); // }, [selectedComment]); const handlePointerUp = (event: React.PointerEvent) => { if (!dragging) return; setDragging(false); const wrapper = wrapperRef.current; if (wrapper) wrapper.releasePointerCapture(event.pointerId); }; const handleCreateComments = async (e: any) => { e.preventDefault(); if (!value) return; if (!threadSocket?.connected && mode === "create") { // API addCommentsApi(projectId, value, selectedComment?.threadId, selectedVersion?.versionId || "").then((createComments) => { if (createComments.message === "Thread comments add Successfully" && createComments.data) { const commentData = { replyId: `${createComments.data?._id}`, creatorId: `${selectedComment?.threadId}`, createdAt: "2 hrs ago", lastUpdatedAt: "2 hrs ago", comment: value, }; setMessages((prevMessages) => [...prevMessages, commentData]); addReply(selectedComment?.threadId, commentData); } }); } else if (threadSocket?.connected && mode === "create") { // SOCKET const addComment = { versionId: selectedVersion?.versionId || "", projectId, userId, comment: value, organization, threadId: selectedComment?.threadId, }; threadSocket.emit("v1-Comment:add", addComment); } setInputActive(false); }; const handleDeleteThread = async () => { if (!projectId) return; if (!threadSocket?.connected) { // API deleteThreadApi(projectId, selectedComment?.threadId, selectedVersion?.versionId || "").then((deleteThread) => { if (deleteThread.message === "Thread deleted Successfully") { removeComment(selectedComment?.threadId); setSelectedComment(null); } }); } else { // SOCKET const deleteThread = { projectId, userId, organization, threadId: selectedComment?.threadId, versionId: selectedVersion?.versionId || "", }; setSelectedComment(null); removeComment(selectedComment?.threadId); threadSocket.emit("v1:thread:delete", deleteThread); } }; const handleCreateThread = async (e: any) => { e.preventDefault(); if (!projectId) return; if (!threadSocket?.connected) { // API createThreadApi(projectId, "active", commentPositionState.position, [0, 0, 0], value, selectedVersion?.versionId || "").then((thread) => { if (thread.message === "Thread created Successfully" && thread?.threadData) { const comment: ThreadSchema = { state: "active", threadId: thread?.threadData?._id, creatorId: userId, createdAt: getRelativeTime(thread.threadData?.createdAt), threadTitle: value, lastUpdatedAt: new Date().toISOString(), position: commentPositionState.position, rotation: [0, 0, 0], comments: [], }; addComment(comment); setCommentPositionState(null); setInputActive(false); setSelectedComment(null); } }); } else { // SOCKET const createThread = { projectId, versionId: selectedVersion?.versionId || "", userId, organization, state: "active", position: commentPositionState.position, rotation: [0, 0, 0], threadTitle: value, }; setCommentPositionState(null); setInputActive(false); setSelectedComment(null); threadSocket.emit("v1:thread:create", createThread); } }; const scrollToBottom = () => { const messagesWrapper = document.querySelector(".messages-wrapper"); if (messagesWrapper) { messagesWrapper.scrollTop = messagesWrapper.scrollHeight; } }; useEffect(() => { if (messages.length > 0) scrollToBottom(); }, [messages]); return (
handlePointerMove({ clientX: e.clientX, clientY: e.clientY })} onPointerUp={handlePointerUp} style={{ position: "absolute", left: position.x, top: position.y, cursor: dragging ? "grabbing" : "grab", userSelect: "none", zIndex: 9999, }} >
Comment
{ e.preventDefault(); setOpenThreadOptions((prev) => !prev); }} >
{openThreadOptions && (
Mark as Unread
Mark as Resolved
Delete Thread
)}
{selectedComment && } {messages.map((val, i) => ( ))}